diff --git a/.github/workflows/unit-test-on-pull-request.yml b/.github/workflows/unit-test-on-pull-request.yml new file mode 100644 index 00000000..79cd4c4b --- /dev/null +++ b/.github/workflows/unit-test-on-pull-request.yml @@ -0,0 +1,120 @@ +name: otel-profiling-agent + +on: + push: + branches: [main] + pull_request: + branches: ["**"] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + fail-fast: true + max-parallel: 2 + matrix: + go: ["stable"] + steps: + - name: Install dependencies + run: sudo apt-get install -y llvm clang dwz cmake curl unzip + - name: Install Zydis + shell: bash + run: | + cd /tmp + git clone --depth 1 --branch v3.1.0 --recursive https://github.com/zyantific/zydis.git + cd zydis + rm -rf build + mkdir build + cd build + cmake -DZYDIS_BUILD_EXAMPLES=OFF .. + make -j$(nproc) + sudo make install + cd zycore + sudo make install + - name: Set up Go ${{matrix.go}} + uses: actions/setup-go@v5 + with: + go-version: ${{matrix.go}} + check-latest: true + cache-dependency-path: | + go.sum + id: go + - name: Install gRPC dependencies + env: + PB_URL: "https://github.com/protocolbuffers/protobuf/releases/download/v24.4/" + PB_FILE: "protoc-24.4-linux-x86_64.zip" + INSTALL_DIR: "/usr/local" + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.31.0 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 + curl -LO "$PB_URL/$PB_FILE" + sudo unzip "$PB_FILE" -d "$INSTALL_DIR" 'bin/*' 'include/*' + sudo chmod +xr "$INSTALL_DIR/bin/protoc" + sudo find "$INSTALL_DIR/include" -type d -exec chmod +x {} \; + sudo find "$INSTALL_DIR/include" -type f -exec chmod +r {} \; + rm "$PB_FILE" + - name: Check out + uses: actions/checkout@v4 + - 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 + make lint + + test: + name: Test + runs-on: ubuntu-latest + strategy: + fail-fast: true + max-parallel: 2 + matrix: + go: ["stable"] + steps: + - name: Install dependencies + run: sudo apt-get install -y llvm clang dwz cmake curl unzip + - name: Install Zydis + shell: bash + run: | + cd /tmp + git clone --depth 1 --branch v3.1.0 --recursive https://github.com/zyantific/zydis.git + cd zydis + rm -rf build + mkdir build + cd build + cmake -DZYDIS_BUILD_EXAMPLES=OFF .. + make -j$(nproc) + sudo make install + cd zycore + sudo make install + - name: Set up Go ${{matrix.go}} + uses: actions/setup-go@v5 + with: + go-version: ${{matrix.go}} + check-latest: true + cache-dependency-path: | + go.sum + id: go + - name: Install gRPC dependencies + env: + PB_URL: "https://github.com/protocolbuffers/protobuf/releases/download/v24.4/" + PB_FILE: "protoc-24.4-linux-x86_64.zip" + INSTALL_DIR: "/usr/local" + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.31.0 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 + curl -LO "$PB_URL/$PB_FILE" + sudo unzip "$PB_FILE" -d "$INSTALL_DIR" 'bin/*' 'include/*' + sudo chmod +xr "$INSTALL_DIR/bin/protoc" + sudo find "$INSTALL_DIR/include" -type d -exec chmod +x {} \; + sudo find "$INSTALL_DIR/include" -type f -exec chmod +r {} \; + rm "$PB_FILE" + - name: Check out + uses: actions/checkout@v4 + - name: Build + run: | + echo $PATH + make test + - name: Tests + run: | + make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..497bce68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.o +*.pb.go +.idea +otel-profiling-agent +tracer.ebpf +tracer.ebpf.arm64 +tracer.ebpf.x86 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..365cd445 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,150 @@ +service: + golangci-lint-version: 1.54.x + +run: + skip-dirs: + - artifacts + - build-targets + - design + - docker-images + - docs + - etc + - experiments + - infrastructure + - legal + - libpf-rs + - mocks + - pf-code-indexing-service/cibackend/gomock_* + - pf-debug-metadata-service/dmsbackend/gomock_* + - pf-host-agent/support/ci-kernels + - pf-storage-backend/storagebackend/gomock_* + - scratch + - systemtests/benchmarks/_outdata + - target + - virt-tests + - vm-images + +linters: + enable-all: true + disable: + # Disabled because of + # - too many non-sensical warnings + # - not relevant for us + # - false positives + # + # "might be worth fixing" means we should investigate/fix in the mid term + - containedctx # might be worth fixing + - contextcheck # might be worth fixing + - cyclop + - depguard + - dupword + - durationcheck # might be worth fixing + - errname # might be worth fixing + - errorlint # might be worth fixing + - exhaustive + - exhaustivestruct + - exhaustruct + - forcetypeassert # might be worth fixing + - funlen + - gci # might be worth fixing + - gochecknoglobals + - gochecknoinits + - gocognit + - gocyclo + - godot + - godox # complains about TODO etc + - goerr113 + - gofumpt + - goimports # might be worth fixing + - golint # might be worth fixing + - gomnd + - gomoddirectives + - ifshort + - interfacebloat + - ireturn + - maintidx + - makezero + - nestif + - nilerr # might be worth fixing + - nilnil + - nlreturn + - noctx # might be worth fixing + - nolintlint + - nonamedreturns + - nosnakecase + - paralleltest + - scopelint # might be worth fixing + - sqlclosecheck # might be worth fixing + - tagalign + - tagliatelle + - testableexamples # might be worth fixing + - testpackage + - tparallel # might be worth fixing + - thelper + - varnamelen + - wastedassign + - wsl + - wrapcheck + - forbidigo + # the following linters are deprecated + - exhaustivestruct + - scopelint + - nosnakecase + - interfacer + - maligned + - ifshort + - structcheck # might be worth fixing + - deadcode + - golint + - varcheck + +linters-settings: + goconst: + min-len: 2 + min-occurrences: 2 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - dupImport # https://github.com/go-critic/go-critic/issues/845 + - ifElseChain + - octalLiteral + - whyNoLint + - wrapperFunc + - sloppyTestFuncName + - sloppyReassign + - uncheckedInlineErr # Experimental rule with high false positive rate. + + # Broken with Go 1.18 feature (https://github.com/golangci/golangci-lint/issues/2649): + - hugeParam + - rangeValCopy + - typeDefFirst + - paramTypeCombine + gocyclo: + min-complexity: 15 + govet: + enable-all: true + check-shadowing: true + disable: + - fieldalignment + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - debug,debugf,debugln + - error,errorf,errorln + - fatal,fatalf,fataln + - info,infof,infoln + - log,logf,logln + - warn,warnf,warnln + - print,printf,println,sprint,sprintf,sprintln,fprint,fprintf,fprintln + lll: + line-length: 100 + tab-width: 4 + maligned: + suggest-new: true + misspell: + locale: US diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..6b149f1b --- /dev/null +++ b/AUTHORS @@ -0,0 +1,28 @@ +## Open-Source contributors + +For contributors statistics that occurred after the profiling agent was open sourced, please refer to [the GitHub statistics](https://github.com/elastic/otel-profiling-agent/pulse). + +## Pre-OSS Optimyze/Elastic contributors + +Adam Gowdiak +Adrien Mannocci +Caue Marcondes +Christos Kalkanis +Damien Mathieu +Daniel Mitterdorfer +Florian Lehner +Francesco Gualazzi +Israel Ogbole +Jan Calanog +Joel Höner +Joe Rowell +Joseph Crail +Joseph Kruskal +M.J. Fieggen (Joni) +Sean Heelan +Stephanie Boomsma +Thomas Dullien +Timo Teräs +Tim Rühsen +Victor Martinez +Victor Michel diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7b3e2d23 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM debian:testing + +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 linux-headers-$arch unzip + +RUN git clone --depth 1 --branch v3.1.0 --recursive https://github.com/zyantific/zydis.git && \ + cd zydis && mkdir build && cd build && \ + cmake -DZYDIS_BUILD_EXAMPLES=OFF .. && make -j$(nproc) && make install && \ + 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 + + +# gRPC dependencies +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.31.0 +RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 + +RUN \ + PB_URL="https://github.com/protocolbuffers/protobuf/releases/download/v24.4/"; \ + PB_FILE="protoc-24.4-linux-x86_64.zip"; \ + INSTALL_DIR="/usr/local"; \ + \ + curl -LO "$PB_URL/$PB_FILE" \ + && unzip "$PB_FILE" -d "$INSTALL_DIR" 'bin/*' 'include/*' \ + && chmod +xr "$INSTALL_DIR/bin/protoc" \ + && find "$INSTALL_DIR/include" -type d -exec chmod +x {} \; \ + && find "$INSTALL_DIR/include" -type f -exec chmod +r {} \; \ + && rm "$PB_FILE" + +RUN echo "export PATH=\"\$PATH:\$(go env GOPATH)/bin\"\nexport KERNEL_HEADERS=\"/lib/modules/$(ls /lib/modules)\"" >> ~/.bashrc + +ENTRYPOINT ["/bin/bash", "-l", "-c"] diff --git a/KNOWN_KERNEL_LIMITATIONS.md b/KNOWN_KERNEL_LIMITATIONS.md new file mode 100644 index 00000000..1c35d5a6 --- /dev/null +++ b/KNOWN_KERNEL_LIMITATIONS.md @@ -0,0 +1,41 @@ +Known limitations +================= +The Linux kernel is constantly evolving and so is eBPF. To be able to load our eBPF code with older kernel versions we have to write code to avoid some limitations. This file documents the restrictions we ran into while writing the code. + +Number of tracepoints +--------------------- +Affects kernel < 4.15. + +There was a limit of 1 eBPF program per tracepoint/kprobe. +This limit no longer holds and was removed with commit [e87c6bc3852b981e71c757be20771546ce9f76f3](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e87c6bc3852b981e71c757be20771546ce9f76f3). + + +Obtaining Kernel backtrace +-------------------------- +Affects kernel < 4.18 + +It is not possible to get individual backtraces from kernel mode stack with bpf_get_stackid(). It returns hash of the backtrace, and if it collides with another backtrace before the agent has collected it, we might report wrong kernel backtracec. +A more suitable helper bpf_get_stack() was added in commit [c195651e565ae7f41a68acb7d4aa7390ad215de1](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c195651e565ae7f41a68acb7d4aa7390ad215de1). + + +Kernel version check +-------------------- +Affects kernel < 5.0. + +As part of the verification of eBPF programs, the `kern_version` attribute was checked and it needed to match with the currently running kernel version. +This check was removed with commit [6c4fc209fcf9d27efbaa48368773e4d2bfbd59aa](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6c4fc209fcf9d27efbaa48368773e4d2bfbd59aa). + + +eBPF instruction limit +---------------------- +Affects kernel < 5.2. + +The number of eBPF instructions per program was limited to 4096 instructions. +This limit was raised to 1 million eBPF instructions with commit [c04c0d2b968ac45d6ef020316808ef6c82325a82](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c04c0d2b968ac45d6ef020316808ef6c82325a82). + + +eBPF inner arrays (map-in-map) must be of same size +--------------------------------------------------- +Affects kernel < 5.10. + +This restriction was removed with commit[4a8f87e60f6db40e640f1db555d063b2c4dea5f1](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4a8f87e60f6db40e640f1db555d063b2c4dea5f1). diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b6c7efb9 --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +.PHONY: all all-common binary clean ebpf generate test test-deps protobuf docker-image agent legal + +SHELL:=/usr/bin/env bash + +all: generate ebpf binary + +# Removes the go build cache and binaries in the current project +clean: + go clean -cache -i + $(MAKE) -C support/ebpf clean + rm -f build-targets/*.{deb,rpm} + rm -f support/*.test + +generate: protobuf + go install github.com/florianl/bluebox@v0.0.1 + go generate ./... + +binary: + go build -buildvcs=false -ldflags="-extldflags=-static" -tags osusergo,netgo + +ebpf: + $(MAKE) -j$(shell nproc) -C support/ebpf + +lint: generate + # We don't want to build the tracers here, so we stub them for linting + touch support/ebpf/tracer.ebpf.x86 + golangci-lint run --timeout 10m + +test: generate ebpf test-deps + go test ./... + +TESTDATA_DIRS:= \ + libpf/nativeunwind/elfunwindinfo/testdata \ + libpf/pfelf/testdata \ + reporter/testdata + +test-deps: + $(foreach testdata_dir, $(TESTDATA_DIRS), \ + ($(MAKE) -C "$(testdata_dir)") || exit ; \ + ) + +protobuf: + cd proto && ./buildpb.sh + +# Detect native architecture. +UNAME_NATIVE_ARCH:=$(shell uname -m) + +ifeq ($(UNAME_NATIVE_ARCH),x86_64) +NATIVE_ARCH:=amd64 +else ifeq ($(UNAME_NATIVE_ARCH),aarch64) +NATIVE_ARCH:=arm64 +else +$(error Unsupported architecture: $(UNAME_NATIVE_ARCH)) +endif + +docker-image: + docker build -t profiling-agent --build-arg arch=$(NATIVE_ARCH) -f Dockerfile . + +agent: + docker run -v "$$PWD":/agent -it profiling-agent make + +legal: + @go install go.elastic.co/go-licence-detector@latest + @go list -m -json $(sort $(shell go list -deps -tags=linux -f "{{with .Module}}{{if not .Main}}{{.Path}}{{end}}{{end}}" .)) | go-licence-detector \ + -includeIndirect \ + -rules legal/rules.json \ + -depsTemplate=legal/templates/deps.csv.tmpl \ + -depsOut=deps.profiling-agent.csv + @./legal/append-non-go-info.sh legal/non-go-dependencies.json deps.profiling-agent.csv + @echo "Dependencies license summary (from deps.profiling-agent.csv):" + @echo " Count License" + @tail -n '+2' deps.profiling-agent.csv | cut -d',' -f5 | sort | uniq -c | sort -k1rn diff --git a/README.md b/README.md new file mode 100644 index 00000000..3021e164 --- /dev/null +++ b/README.md @@ -0,0 +1,504 @@ +# Introduction + +This repository contains the profiling host agent, extracted from the +Elastic Universal Profiling private repository. + +The profiling host agent gathers trace counts and pushes them to the reporter +subsystem. It loads an eBPF program, and its related maps, from an ELF file then +monitors those maps for new traces and trace count updates. + +# TODO + +See open [issues](https://github.com/elastic/otel-profiling-agent/issues). + +# Legal + +## Licensing Information + +This project is licensed under the Apache License 2.0 (Apache-2.0). +[Apache License 2.0](LICENSE) + +The eBPF source code is licensed under the GPL 2.0 license. +[GPL 2.0](support/ebpf/LICENSE) + +## Licenses of dependencies + +To display a summary of the dependencies' licenses: +```sh +make legal +``` + +Details can be found in the generated `deps.profiling-agent.csv` file. + +At the time of writing this, the summary is +``` + Count License + 52 Apache-2.0 + 17 BSD-3-Clause + 17 MIT + 3 BSD-2-Clause + 1 ISC +``` + +# The Profiling Agent + +## Building + +The agent can be built without affecting your environment by using the provided +`make` targets. You need to have `docker` installed, though. +Builds on amd64 and arm64 architectures are supported. + +The first step is to build the Docker image that contains the build environment: +```sh +make docker-image +``` + +Then, you can build the agent: +```sh +make agent +``` + +The resulting binary will be in the current directory as `otel-profiling-agent`. + +Alternatively, you can build without Docker. Please see the `Dockerfile` for required dependencies. + +After installing the dependencies, just run `make` to build. + +## Running + +You can start the agent with the following command: + +```sh +sudo ./otel-profiling-agent -collection-agent=127.0.0.1:11000 -disable-tls +``` + +The agent comes with a functional but work-in-progress / evolving implementation +of the recently released OTel profiling [signal](https://github.com/open-telemetry/opentelemetry-proto/pull/534). + +The agent loads the eBPF program and its maps, starts unwinding and reports +captured traces to the backend. + +## Visualizing data locally + +We created a desktop application called "devfiler" that allows visualizing the +profiling agent's output locally, making it very convenient for development use. +devfiler spins up a local server that listens on `0.0.0.0:11000`. + +![Screenshot of devfiler UI](./docs/devfiler.png) + +To run it, simply download and unpack the archive from the following URL: + +https://upload.elastic.co/d/d7d7ad8209a3a67967140e7225bba87095de5b8f8adf3842a575344e4b5eff9e + +Authentication token: `6b098a7be41406fd` + +The archive contains a build for each of the following platforms: + +- macOS (Intel) +- macOS (Apple Silicon) +- Linux AppImage (x86_64) +- Linux AppImage (aarch64) + +> [!NOTE] +> devfiler is currently in an experimental preview stage. + +### macOS + +This build of devfiler is currently not signed with a globally trusted Apple +developer ID, but with a developer certificate. If you simply double-click the +application, you'll run into an error. Instead of opening it with a double +click, simply do a **right-click** on `devfiler.app`, then choose "Open". If you +go this route, you'll instead be presented with the option to run it anyway. + +### Linux + +The AppImages in the archive should run on any Linux distribution with a +reasonably modern glibc and libgl installation. To run the application, simply +extract the archive and then do: + +```console +./devfiler-appimage-$(uname -m).AppImage +``` + +## Agent internals + +The host agent is a Go application that is deployed to all machines customers +wish to profile. It collects, processes and pushes observed stack traces and +related meta-information to a backend collector. + +### Concepts + +#### File IDs + +A file ID uniquely identifies an executable, kernel or script language source +file. + +File IDs for native applications are created by taking the SHA256 checksum of a +file's head, tail, and size, then truncating the hash digest to 16 bytes (128 +bits): + +``` +Input ← Concat(File[:4096], File[-4096:], BigEndianUInt64(Len(File))) +Digest ← SHA256(Input) +FileID ← Digest[:16] +``` + +File IDs for script and JIT languages are created in an interpreter-specific +fashion. + +File IDs for Linux kernels are calculated by taking the FNV128 hash of their GNU +build ID. + +#### Stack unwinding + +Stack unwinding is the process of recovering the list of function calls that +lead execution to the point in the program at which the profiler interrupted it. + +How stacks are unwound varies depending on whether a thread is running native, +JITed or interpreted code, but the basic idea is always the same: every language +that supports arbitrarily nested function calls needs a way to keep track of +which function it needs to return to after the current function completes. Our +unwinder uses that same information to repeatedly determine the caller until we +reach the thread's entry point. + +In simplified pseudo-code: + +``` +pc ← interrupted_process.cpu.pc +sp ← interrupted_process.cpu.sp + +while !is_entry_point(pc): + file_id, start_addr, interp_type ← file_id_at_pc(pc) + push_frame(interp_type, file_id, pc - start_addr) + unwinder ← unwinder_for_interp(interp_type) + pc, sp ← unwinder.next_frame(pc, sp) +``` + +#### Symbolization + +Symbolization is the process of assigning source line information to the raw +addresses extracted during stack unwinding. + +For script and JIT languages that always have symbol information available on +the customer machines, the host agent is responsible for symbolizing frames. + +For native code the symbolization occurs in the backend. Stack frames are sent +as file IDs and the offset within the file and the symbolization service is then +responsible for assigning the correct function name, source file and lines in +the background. Symbols for open-source software installed from OS package repos +are pulled in from our global symbolization infrastructure and symbols for +private executables can be manually uploaded by the customer. + +The primary reason for doing native symbolization in the backend is that native +executables in production will often be stripped. Asking the customer to deploy +symbols to production would be both wasteful in terms of disk usage and also a +major friction point in initial adoption. + +#### Stack trace representation + +We have two major representations for our stack traces. + +The raw trace format produced by our BPF unwinders: + +https://github.com/elastic/otel-profiling-agent/blob/385bcd5273fae22cdc2cf74bacae6a54fe6ce153/host/host.go#L54-L60 + +The final format produced after additional processing in user-land: + +https://github.com/elastic/otel-profiling-agent/blob/385bcd5273fae22cdc2cf74bacae6a54fe6ce153/libpf/libpf.go#L452-L457 + +The two might look rather similar at first glance, but there are some important differences: + +- the BPF variant uses truncated 64-bit file IDs to save precious kernel memory +- for interpreter frames the BPF variant uses the file ID and line number fields to store + more or less arbitrary interpreter-specific data that is needed by the user-mode code to + conduct symbolization + +A third trace representation exists within our network protocol, but it essentially +just a deduplicated, compressed representation of the user-land trace format. + +#### Trace hashing + +In profiling it is common to see the same trace many times. Traces can be up to +128 entries long, and repeatedly symbolizing and sending the same traces over the +network would be very wasteful. We use trace hashing to avoid this. Different +hashing schemes are used for the BPF and user-mode trace representations. Multiple +64 bit hashes can end up being mapped to the same 128 bit hash, but *not* vice-versa. + +**BPF trace hash (64 bit):** + +``` +H(kernel_stack_id, frames_user, PID) +``` + +**User-land trace hash (128 bit)** + +``` +H(frames_user_kernel) +``` + +### User-land sub-components + +#### Tracer + +The tracer is a central user-land component that loads and attaches our BPF +programs to their corresponding BPF probes during startup and then continues to +serve as the primary event pump for BPF <-> user-land communication. It further +instantiates and owns other important subcomponents like the process manager. + +#### Trace handler + +The trace handler is responsible for converting traces from the BPF format to +the user-space format. It receives raw traces [tracer](#tracer), converts them +to the user-space format and then sends them on to the [reporter](#reporter). +The majority of the conversion logic happens via a call into the process +manager's [`ConvertTrace`] function. + +Since converting and enriching BPF-format traces is not a cheap operation, the +trace handler is also responsible for keeping a cache (mapping) of trace hashes: +from 64bit BPF hash to the user-space 128bit hash. + +[`ConvertTrace`]: https://github.com/elastic/otel-profiling-agent/blob/385bcd5273fae22cdc2cf74bacae6a54fe6ce153/processmanager/manager.go#L205 + +#### Reporter + +The reporter receives traces and trace counts in the user-mode format from the +[trace handler](#trace-handler), converts them to the gRPC representation and +then sends them out to a backend collector. + +It also receives additional meta-information (such as [metrics](metrics/metrics.json) and [host metadata](hostmetadata/hostmetadata.json)) +which it also converts and sends out to a backend collector over gRPC. + +The reporter does not offer strong guarantees regarding reliability of +network operations and may drop data at any point, an "eventual consistency" +model. + +#### Process manager + +The process manager receives process creation/termination events from +[tracer](#tracer) and is responsible for making available any information to the +BPF code that it needs to conduct unwinding. It maintains a map of the +executables mapped into each process, loads stack unwinding deltas for native +modules and creates interpreter handlers for each memory mapping that belongs to +a supported language interpreter. + +During trace conversion the process manager is further responsible for routing +symbolization requests to the correct interpreter handlers. + +#### Interpreter handlers + +Each interpreted or JITed language that we support has a corresponding type that +implements the interpreter handler interface. It is responsible for: + +- detecting the interpreter's version and structure layouts +- placing information that the corresponding BPF interpreter unwinder needs into BPF maps +- translating interpreter frames from the BPF format to the user-land format by symbolizing them + +#### Stack delta provider + +Unwinding the stack of native executables compiled without frame pointers +requires stack deltas. These deltas are essentially a mapping from each PC in an +executable to instructions describing how to find the caller and how to adjust +the unwinder machine state in preparation of locating the next frame. Typically +these instructions consist of a register that is used as a base address and an +offset (delta) that needs to be added to it -- hence the name. The stack delta +provider is responsible for analyzing executables and creating stack deltas for +them. + +For most native executables, we rely on the information present in `.eh_frame`. +`.eh_frame` was originally meant only for C++ exception unwinding, but it has +since been repurposed for stack unwinding in general. Even applications written +in many other native languages like C, Zig or Rust will typically come with +`.eh_frame`. + +One important exception to this general pattern is Go. As of writing, Go +executables do not come with `.eh_frame` sections unless they are built with CGo +enabled. Even with CGo the `.eh_frame` section will only contain information for +a small subset of functions that are either written in C/C++ or part of the CGo +runtime. For Go executables we extract the stack delta information from the +Go-specific section called `.gopclntab`. In-depth documentation on the format is +available in [a separate document](docs/gopclntab.md)). + +### BPF components + +The BPF portion of the host agent implements the actual stack unwinding. It uses +the eBPF virtual machine to execute our code directly in the Linux kernel. The +components are implemented in BPF C and live in the +[`otel-profiling-agent/support/ebpf`](./support/ebpf) directory. + +#### Limitations + +BPF programs must adhere to various restrictions imposed by the verifier. Many +of these limitations are significantly relaxed in newer kernel versions, but we +still have to stick to the old limits because we wish to continue supporting +older kernels. + +The minimum supported Linux kernel versions are +- 4.19 for amd64/x86_64 +- 5.5 for arm64/aarch64 + +The most notable limitations are the following two: + +- **4096 instructions per program**\ + A single BPF program can consist of a maximum of 4096 instructions, otherwise + older kernels will refuse to load it. Since BPF does not allow for loops, they + instead need to be unrolled. +- **32 tail-calls**\ + Linux allows BPF programs to do a tail-call to another BPF program. A tail + call is essentially a `jmp` into another BPF program, ending execution of the + current handler and starting a new one. This allows us to circumvent the 4096 + instruction limit a bit by doing a tail-call before we run into the limit. + There's a maximum of 32 tail calls that a BPF program can do. + +These limitations mean that we generally try to prepare as much work as possible +in user-land and then only do the minimal work necessary within BPF. We can only +use $O(\log{n})$ algorithms at worst and try to stick with $O(1)$ for most things. +All processing that cannot be implemented like this must be delegated to +user-land. As a general rule of thumb, anything that needs more than 32 +iterations in a loop is out of the question for BPF. + +#### Unwinders + +Unwinding always begins in [`native_tracer_entry`]. This entry point for our +tracer starts by reading the register state of the thread that we just +interrupted and initializes the [`PerCPURecord`] structure. The per-CPU record +persists data between tail-calls of the same unwinder invocation. The unwinder's +current `PC`, `SP` etc. values are initialized from register values. + +After the initial setup the entry point consults a BPF map that is maintained +by the user-land portion of the agent to determine which interpreter unwinder +is responsible for unwinding the code at `PC`. If a record for the memory +region is found, we then tail-call to the corresponding interpreter unwinder. + +Each interpreter unwinder has their own BPF program. The interpreter unwinders +typically have an unrolled main loop where they try to unwind as many frames for +that interpreter as they can without going over the instruction limit. After +each iteration the unwinders will typically check whether the current PC value +still belongs to the current unwinder and tail-call to the right unwinder +otherwise. + +When an unwinder detects that we've reached the last frame in the trace, +unwinding is terminated with a tail call to [`unwind_stop`]. For most traces +this call will happen in the native unwinder, since even JITed languages +usually call through a few layers of native C/C++ code before entering the VM. +We detect the end of a trace by heuristically marking certain functions with +`PROG_UNWIND_STOP` in the BPF maps prepared by user-land. `unwind_stop` then +sends the completed BPF trace to user-land. + +If any frame in the trace requires symbolization in user-mode, we additionally +send a BPF event to request an expedited read from user-land. For all other +traces user-land will simply read and then clear this map on a timer. + +[`native_tracer_entry`]: https://github.com/elastic/otel-profiling-agent/blob/385bcd5273fae22cdc2cf74bacae6a54fe6ce153/support/ebpf/native_stack_trace.ebpf.c#L875 +[`PerCPURecord`]: https://github.com/elastic/otel-profiling-agent/blob/385bcd5273fae22cdc2cf74bacae6a54fe6ce153/support/ebpf/types.h#L576 +[`unwind_stop`]: https://github.com/elastic/otel-profiling-agent/blob/385bcd5273fae22cdc2cf74bacae6a54fe6ce153/support/ebpf/interpreter_dispatcher.ebpf.c#L125 + +#### PID events + +The BPF components are responsible for notifying user-land about new and exiting +processes. An event about a new process is produced when we first interrupt it +with the unwinders. Events about exiting processes are created with a +`sched_process_exit` probe. In both cases the BPF code sends a perf event to +notify user-land. We also re-report a PID if we detect execution in previously +unknown memory region to prompt re-scan of the mappings. + +### Network protocol + +All collected information is reported to a backend collector via a push-based, +stateless, one-way gRPC [protocol](https://github.com/open-telemetry/opentelemetry-proto/pull/534). + +All data to be transmitted is stored in bounded FIFO queues (ring buffers). Old +data is overwritten when the queues fill up (e.g. due to a lagging or offline +backend collector). There is no explicit reliability or redundancy (besides +retries internal to gRPC) and the assumption is that data will be resent +(eventually consistent). + +### Trace processing pipeline + +The host agent contains an internal pipeline that incrementally processes the +raw traces that are produced by the BPF unwinders, enriches them with additional +information (e.g. symbols for interpreter frames and container info), deduplicates +known traces and combines trace counts that occurred in the same update period. + +The traces produced in BPF start out with the information shown in the following +diagram. + +
+Note: please read this if you wish to update the diagrams + +The diagrams in this section were created via draw.io. The SVGs can be loaded +into draw.io for editing. When you're done, make sure to export via +File -> Export As -> SVG and then select +a zoom level of 200%. If you simply save the diagram via CTRL+S, +it won't fill the whole width of the documentation page. Also make sure that +"Include a copy of my diagram" remains ticked to keep the diagram editable. + +
+ +![bpf-trace-diagram](docs/bpf-trace.drawio.svg) + +Our backend collector expects to receive trace information in a normalized and +enriched format. This diagram below is relatively close to the data-structures +that are actually sent over the network, minus the batching and domain-specific +deduplication that we apply prior to sending it out. + +![net-trace-diagram](docs/network-trace.drawio.svg) + +The diagram below provides a detailed overview on how the various components of +the host agent interact to transform raw traces into the network format. It +is focused around our data structures and how data flows through them. Dotted +lines represent indirect interaction with data structures, solid ones correspond +to code flow. "UM" is short for "user mode". + +![trace-pipe-diagram](docs/trace-pipe.drawio.svg) + +### Testing strategy + +The host agent code is tested with three test suites: + +- **Go unit tests**\ + Functionality of individual functions and types is tested with regular Go unit + tests. This works great for the user-land portion of the agent, but is unable + to test any of the unwinding logic and BPF interaction. +- **coredump test suite**\ + The coredump test suite (`utils/coredump`) we compile the whole BPF unwinder + code into a user-mode executable, then use the information from a coredump to + simulate a realistic environment to test the unwinder code in. The coredump + suite essentially implements all required BPF helper functions in user-space, + reading memory and thread contexts from the coredump. The resulting traces are + then compared to a frame list in a JSON file, serving as regression tests. +- **BPF integration tests**\ + A special build of the host agent with the `integration` tag is created that + enables specialized test cases that actually load BPF tracers into the kernel. + These test cases require root privileges and thus cannot be part of the + regular unit test suite. The test cases focus on covering the interaction and + communication of BPF with user-mode code, as well as testing that our BPF code + passes the BPF verifier. Our CI builds the integration test executable once and + then executes it on a wide range of different Linux kernel versions via qemu. + +### Probabilistic profiling + +Probabilistic profiling allows you to reduce storage costs by collecting a representative +sample of profiling data. This method decreases storage costs with a visibility trade-off, +as not all Profiling Host Agents will have profile collection enabled at all times. + +Profiling Events linearly correlate with the probabilistic profiling value. The lower the value, +the fewer events are collected. + +#### Configure probabilistic profiling + +To configure probabilistic profiling, set the `-probabilistic-threshold` and `-probabilistic-interval` options. + +Set the `-probabilistic-threshold` option to a unsigned integer between 1 and 99 to enable + probabilistic profiling. At every probabilistic interval, a random number between 0 and 99 is chosen. + If the probabilistic threshold that you've set is greater than this random number, the agent collects + profiles from this system for the duration of the interval. The default value is 100. + +Set the `-probabilistic-interval` option to a time duration to define the time interval for which +probabilistic profiling is either enabled or disabled. The default value is 1 minute. + +#### Example + +The following example shows how to configure the profiling agent with a threshold of 50 and an interval of 2 minutes and 30 seconds: +```bash +sudo ./otel-profiling-agent -probabilistic-threshold=50 -probabilistic-interval=2m30s +``` diff --git a/cli_flags.go b/cli_flags.go new file mode 100644 index 00000000..68a5941e --- /dev/null +++ b/cli_flags.go @@ -0,0 +1,241 @@ +/* + * 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 ( + "flag" + "fmt" + "os" + "runtime" + "strings" + "time" + + cebpf "github.com/cilium/ebpf" + "github.com/peterbourgon/ff/v3" + + "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" +) + +const ( + // Default values for CLI flags + defaultArgSamplesPerSecond = 20 + defaultArgReporterInterval = 5.0 * time.Second + defaultArgMonitorInterval = 5.0 * time.Second + defaultArgPrivateMachineID = "" + defaultArgPrivateEnvironmentType = "" + defaultProbabilisticThreshold = tracer.ProbabilisticThresholdMax + defaultProbabilisticInterval = 1 * time.Minute + defaultArgSendErrorFrames = false + + // This is the X in 2^(n + x) where n is the default hardcoded map size value + defaultArgMapScaleFactor = 0 + // 1TB of executable address space + maxArgMapScaleFactor = 8 +) + +// Help strings for command line arguments +var ( + noKernelVersionCheckHelp = "Disable checking kernel version for eBPF support. " + + "Use at your own risk, to run the agent on older kernels with backported eBPF features." + copyrightHelp = "Show copyright and short license text." + collAgentAddrHelp = "The collection agent address in the format of host:port." + verboseModeHelp = "Enable verbose logging and debugging capabilities." + tracersHelp = "Comma-separated list of interpreter tracers to include." + mapScaleFactorHelp = fmt.Sprintf("Scaling factor for eBPF map sizes. "+ + "Every increase by 1 doubles the map size. Increase if you see eBPF map size errors. "+ + "Default is %d corresponding to 4GB of executable address space, max is %d.", + defaultArgMapScaleFactor, maxArgMapScaleFactor) + 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." + secretTokenHelp = "The secret token associated with the project id." + tagsHelp = fmt.Sprintf("User-specified tags separated by ';'. "+ + "Each tag should match '%v'.", host.ValidTagRegex) + disableTLSHelp = "Disable encryption for data in transit." + bpfVerifierLogLevelHelp = "Log level of the eBPF verifier output (0,1,2). Default is 0." + bpfVerifierLogSizeHelp = "Size in bytes that will be allocated for the eBPF " + + "verifier output. Only takes effect if bpf-log-level > 0." + versionHelp = "Show version." + probabilisticThresholdHelp = fmt.Sprintf("If set to a value between 1 and %d will enable "+ + "probabilistic profiling: "+ + "every probabilistic-interval a random number between 0 and %d is "+ + "chosen. If the given probabilistic-threshold is greater than this "+ + "random number, the agent will collect profiles from this system for "+ + "the duration of the interval.", + tracer.ProbabilisticThresholdMax-1, tracer.ProbabilisticThresholdMax-1) + probabilisticIntervalHelp = "Time interval for which probabilistic profiling will be " + + "enabled or disabled." +) + +// Variables for command line arguments +var ( + // Customer-visible flag variables. + argNoKernelVersionCheck bool + argCollAgentAddr string + argCopyright bool + argVersion bool + argTracers string + argVerboseMode bool + argProjectID uint + argCacheDirectory string + argConfigFile string + argSecretToken string + argDisableTLS bool + argTags string + argBpfVerifierLogLevel uint + argBpfVerifierLogSize int + argMapScaleFactor uint + argProbabilisticThreshold uint + argProbabilisticInterval time.Duration + + // "internal" flag variables. + // Flag variables that are configured in "internal" builds will have to be assigned + // a default value here, for their consumption in customer-facing builds. + argEnvironmentType = defaultArgPrivateEnvironmentType + argMachineID = defaultArgPrivateMachineID + argMonitorInterval = defaultArgMonitorInterval + argReporterInterval = defaultArgReporterInterval + argSamplesPerSecond = defaultArgSamplesPerSecond + argSendErrorFrames = defaultArgSendErrorFrames +) + +// Package-scope variable, so that conditionally compiled other components can refer +// to the same flagset. +var fs = flag.NewFlagSet("otel-profiling-agent", flag.ExitOnError) + +func parseArgs() error { + // Please keep the parameters ordered alphabetically in the source-code. + fs.UintVar(&argBpfVerifierLogLevel, "bpf-log-level", 0, bpfVerifierLogLevelHelp) + fs.IntVar(&argBpfVerifierLogSize, "bpf-log-size", cebpf.DefaultVerifierLogSize, + bpfVerifierLogSizeHelp) + + fs.StringVar(&argCacheDirectory, "cache-directory", config.CacheDirectory(), + cacheDirectoryHelp) + fs.StringVar(&argCollAgentAddr, "collection-agent", "", + collAgentAddrHelp) + fs.StringVar(&argConfigFile, "config", "/etc/otel/profiling-agent/agent.conf", + configFileHelp) + fs.BoolVar(&argCopyright, "copyright", false, copyrightHelp) + + fs.BoolVar(&argDisableTLS, "disable-tls", false, disableTLSHelp) + + fs.UintVar(&argMapScaleFactor, "map-scale-factor", + defaultArgMapScaleFactor, mapScaleFactorHelp) + + fs.BoolVar(&argNoKernelVersionCheck, "no-kernel-version-check", false, noKernelVersionCheckHelp) + + fs.UintVar(&argProjectID, "project-id", 1, projectIDHelp) + + // Using a default value here to simplify OTEL review process. + fs.StringVar(&argSecretToken, "secret-token", "abc123", secretTokenHelp) + + fs.StringVar(&argTags, "tags", "", tagsHelp) + fs.StringVar(&argTracers, "t", "all", "Shorthand for -tracers.") + fs.StringVar(&argTracers, "tracers", "all", tracersHelp) + + fs.BoolVar(&argVerboseMode, "v", false, "Shorthand for -verbose.") + fs.BoolVar(&argVerboseMode, "verbose", false, verboseModeHelp) + fs.BoolVar(&argVersion, "version", false, versionHelp) + + fs.UintVar(&argProbabilisticThreshold, "probabilistic-threshold", + defaultProbabilisticThreshold, probabilisticThresholdHelp) + fs.DurationVar(&argProbabilisticInterval, "probabilistic-interval", + defaultProbabilisticInterval, probabilisticIntervalHelp) + + fs.Usage = func() { + fs.PrintDefaults() + } + + err := ff.Parse(fs, os.Args[1:], + ff.WithEnvVarPrefix("OTEL_PROFILING_AGENT"), + ff.WithConfigFileFlag("config"), + ff.WithConfigFileParser(ff.PlainParser), + // This will ignore configuration file (only) options that the current HA + // does not recognize. + ff.WithIgnoreUndefined(true), + ff.WithAllowMissingConfigFile(true), + ) + + 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) { + log.Debug(fmt.Sprintf("%s: %v", f.Name, f.Value)) + }) +} diff --git a/cli_flags_internal.go b/cli_flags_internal.go new file mode 100644 index 00000000..15d0d2cb --- /dev/null +++ b/cli_flags_internal.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. + */ + +// This file contains the CLI flags that are not going to be released to our customers. +// Only builds tagged with 'internal' will contain the additional arguments. +//go:build internal + +package main + +var ( + // Flag variables, pprof-specific + argEnablePProf bool + argSaveCPUProfile bool + + // Help messages + samplesPerSecondHelp = "Set the frequency (in Hz) of stack trace sampling." + reporterIntervalHelp = "Set the reporter's interval in seconds." + monitorIntervalHelp = "Set the monitor interval in seconds." + environmentTypeHelp = "The type of environment." + machineIDHelp = "The machine ID." + exitDelayHelp = "Delay before exiting." + enablePProfHelp = "Enables PProf profiling" + saveCPUProfileHelp = "Save CPU pprof profile to `cpu.pprof`" + sendErrorFramesHelp = "Send error frames (devfiler only, breaks Kibana)" +) + +func init() { + fs.BoolVar(&argEnablePProf, "enable-pprof", false, + enablePProfHelp) + + fs.BoolVar(&argSaveCPUProfile, "save-cpuprofile", false, + saveCPUProfileHelp) + + // It would be nice if we could somehow have a private and a public flagset (and move these + // elements to the private set; sadly flagset.Parse() returns error on unexpected flags, and + // there is no good way to extract the 'already parsed' flags. This leads to a situation where + // we can't parse the command line into two flagsets easily, because we'd somehow need to know + // which arguments belong in the one flagset and which ones belong in the other. + // We put the private flags into 'internal' builds, which we will not distribute to customers. + fs.StringVar(&argEnvironmentType, "private-environment-type", defaultArgPrivateEnvironmentType, + environmentTypeHelp) + fs.StringVar(&argMachineID, "private-machine-id", defaultArgPrivateMachineID, + machineIDHelp) + fs.IntVar(&argSamplesPerSecond, "private-samples-per-second", defaultArgSamplesPerSecond, + samplesPerSecondHelp) + fs.DurationVar(&argReporterInterval, "private-reporter-interval", defaultArgReporterInterval, + reporterIntervalHelp) + fs.DurationVar(&argMonitorInterval, "private-monitor-interval", defaultArgMonitorInterval, + monitorIntervalHelp) + fs.BoolVar(&argSendErrorFrames, "private-send-error-frames", defaultArgSendErrorFrames, + sendErrorFramesHelp) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..d6c97628 --- /dev/null +++ b/config/config.go @@ -0,0 +1,370 @@ +/* + * 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 config + +import ( + "fmt" + "os" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +const ( + traceCacheMinSize = 65536 +) + +// Config is the structure to pass the configuration into host-agent. +type Config struct { + EnvironmentType string + MachineID string + SecretToken string + Tags string + ValidatedTags string + CollectionAgentAddr string + ConfigurationFile string + Tracers string + CacheDirectory string + BpfVerifierLogSize int + BpfVerifierLogLevel uint + MonitorInterval time.Duration + TracePollInterval time.Duration + ReportInterval time.Duration + ProjectID uint32 + SamplesPerSecond uint16 + PresentCPUCores uint16 + DisableTLS bool + UploadSymbols bool + NoKernelVersionCheck bool + TraceCacheIntervals uint8 + Verbose bool + MapScaleFactor uint8 + StartTime time.Time + ProbabilisticInterval time.Duration + ProbabilisticThreshold uint + + // Bits of hostmetadata that we save in config so that they can be + // conveniently accessed globally in the agent. + IPAddress string + Hostname string + KernelVersion string +} + +// Profiling specific variables which are set once at startup of the agent. +// To avoid passing them as argument to every function, they are declared +// on package scope. +var ( + // 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 + // along with traces to identify the project that they belong to. + projectID uint32 + // secretToken is read from the provided configuration file and sent to the collection agent + // along with traces to authenticate the data being sent for a project + secretToken string + // tags contains user-specified tags as passed-in by the user + tags string + // validatedTags contains user-specified tags that have passed validation + validatedTags string + // collectionAgentAddr contains the collection agent address in host:port format + collectionAgentAddr string + // configurationFile contains the path to the profiling agent's configuration file + configurationFile string + // tracers contains the user-specified tracers to enable + tracers string + + // verbose indicates if host agent was started with the verbose argument + verbose bool + // disableTLS indicates if TLS to collection agent is disabled + 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 + // BPF_LOG_LEVEL1 (1) - enable logging + // BPF_LOG_LEVEL2 (2) - more logging + // + // Some older kernels do not handle BPF_LOG_LEVEL2. + bpfVerifierLogLevel uint32 + // bpfVerifierLogSize defines the number of bytes that are pre-allocated to hold the output + // of the eBPF verifier log. + bpfVerifierLogSize int + // maxElementsPerInterval defines the maximum number of possible elements reported per + // monitor interval (MonitorInterval). + maxElementsPerInterval uint32 + + // traceCacheIntervals defines the number of monitor intervals that should fit into the + // tracehandler LRUs. Effectively, this is a sizing factor for those caches. + traceCacheIntervals uint8 + + // samplesPerSecond holds the configured numbers of samples per second. + samplesPerSecond uint16 + + // startTime holds the HA start time + startTime time.Time + + // mapScaleFactor holds a scaling factor for sizing eBPF maps + mapScaleFactor uint8 + + // ipAddress holds the IP address of the interface through which the agent traffic is routed + ipAddress string + + // hostname hosts the hostname of the machine that is running the agent + hostname string + + // kernelVersion holds the kernel release (uname -r) of the machine that is running the agent + kernelVersion string + + // probabilisticThreshold sets the threshold for probabilistic profiling + probabilisticThreshold uint + + // presentCPUCores holds the number of CPU cores + presentCPUCores uint16 +) + +// cacheDirectory is the top level directory that should be used for cache files. These are files +// that can be deleted without loss of data. +var cacheDirectory = "/var/cache/otel/profiling-agent" + +// configurationSet signals that SetConfiguration() has been successfully called and +// the variables it sets can be read. +var configurationSet = false + +func SetConfiguration(conf *Config) error { + var err error + + projectID = conf.ProjectID + + if projectID == 0 || projectID > 4095 { + return fmt.Errorf("invalid project id %d (need > 0 and < 4096)", projectID) + } + + if conf.SecretToken == "" { + return fmt.Errorf("missing SecretToken") + } + secretToken = conf.SecretToken + + tags = conf.Tags + validatedTags = conf.ValidatedTags + verbose = conf.Verbose + samplesPerSecond = conf.SamplesPerSecond + probabilisticThreshold = conf.ProbabilisticThreshold + presentCPUCores = conf.PresentCPUCores + + bpfVerifierLogLevel = uint32(conf.BpfVerifierLogLevel) + bpfVerifierLogSize = conf.BpfVerifierLogSize + + // The environment type (aws/gcp/bare metal) is overridden vs. the default auto-detect. + // WARN: Environment type and machineID are internal flag arguments and not exposed + // in customer-facing builds. + if conf.EnvironmentType != "" { + var environment EnvironmentType + if environment, err = environmentTypeFromString(conf.EnvironmentType); err != nil { + return fmt.Errorf("invalid environment '%s': %s", conf.EnvironmentType, err) + } + + // If the environment is overridden, the machine ID also needs to be overridden. + machineID, err := strconv.ParseUint(conf.MachineID, 0, 64) + if err != nil { + return fmt.Errorf("invalid machine ID '%s': %s", conf.MachineID, err) + } + if machineID == 0 { + return fmt.Errorf( + "the machine ID must be specified with the environment (and non-zero)") + } + log.Debugf("User provided environment (%s) and machine ID (0x%x)", environment, + machineID) + setEnvironment(environment) + hostID = machineID + } else if conf.MachineID != "" { + return fmt.Errorf("you can only specify the machine ID if you also provide the environment") + } + + cacheDirectory = conf.CacheDirectory + if _, err := os.Stat(cacheDirectory); os.IsNotExist(err) { + log.Debugf("Creating cache directory '%s'", cacheDirectory) + if err := os.MkdirAll(cacheDirectory, os.ModePerm); err != nil { + return fmt.Errorf("failed to create cache directory (%s): %s", cacheDirectory, err) + } + } + + collectionAgentAddr = conf.CollectionAgentAddr + configurationFile = conf.ConfigurationFile + disableTLS = conf.DisableTLS + noKernelVersionCheck = conf.NoKernelVersionCheck + uploadSymbols = conf.UploadSymbols + tracers = conf.Tracers + startTime = conf.StartTime + mapScaleFactor = conf.MapScaleFactor + + // Set time values that do not have defaults in times.go + times.reportInterval = conf.ReportInterval + times.monitorInterval = conf.MonitorInterval + times.probabilisticInterval = conf.ProbabilisticInterval + + maxElementsPerInterval = uint32(conf.SamplesPerSecond * + uint16(conf.MonitorInterval.Seconds()) * conf.PresentCPUCores) + traceCacheIntervals = conf.TraceCacheIntervals + + ipAddress = conf.IPAddress + hostname = conf.Hostname + kernelVersion = conf.KernelVersion + + configurationSet = true + return nil +} + +// SamplesPerSecond returns the configured samples per second. +func SamplesPerSecond() uint16 { + return samplesPerSecond +} + +// MaxElementsPerInterval returns the maximum of possible elements reported per interval based on +// the number of cores, samples per second and monitor interval. +func MaxElementsPerInterval() uint32 { + return maxElementsPerInterval +} + +// TraceCacheEntries defines the maximum number of elements for the caches in tracehandler. +// +// The caches in tracehandler have a size-"processing overhead" trade-off: Every cache miss will +// trigger additional processing for that trace in userspace (Go). For most maps, we use +// maxElementsPerInterval as a base sizing factor. For the tracehandler caches, we also multiply +// with traceCacheIntervals. For typical/small values of maxElementsPerInterval, this can lead to +// 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 +func TraceCacheEntries() uint32 { + size := maxElementsPerInterval * uint32(traceCacheIntervals) + if size < traceCacheMinSize { + size = traceCacheMinSize + } + return libpf.NextPowerOfTwo(size) +} + +// Verbose indicates if the agent is running with verbose enabled. +func Verbose() bool { + return verbose +} + +// BpfVerifierLogSetting returns the eBPF verifier log settings. +func BpfVerifierLogSetting() (level uint32, size int) { + return bpfVerifierLogLevel, bpfVerifierLogSize +} + +// HostID returns the hostID of the running agent. The host ID is calculated by calling +// GenerateNewHostIDIfNecessary(). +func HostID() uint64 { + if hostID == 0 { + log.Fatalf("HostID is not set") + } + return hostID +} + +// ProjectID returns the projectID +func ProjectID() uint32 { + if !configurationSet { + log.Fatal("Cannot access ProjectID. Configuration has not been read") + } + return projectID +} + +// SecretToken returns the secretToken associated with the project +func SecretToken() string { + if !configurationSet { + log.Fatal("Cannot access SecretToken. Configuration has not been read") + } + return secretToken +} + +// CacheDirectory returns the cacheDirectory. +func CacheDirectory() string { + return cacheDirectory +} + +// User-specified tags as passed-in by the user +func Tags() string { + return tags +} + +// User-specified tags that have passed validation +func ValidatedTags() string { + return validatedTags +} + +// Collection agent address in host:port format +func CollectionAgentAddr() string { + return collectionAgentAddr +} + +// Path to profiling agent's configuration file +func ConfigurationFile() string { + return configurationFile +} + +// Indicates if TLS to collection agent is disabled +func DisableTLS() bool { + return disableTLS +} + +// Indicates if kernel version checking for eBPF support is disabled +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 +} + +// HA start time +func StartTime() time.Time { + return startTime +} + +// Scaling factor for eBPF maps +func MapScaleFactor() uint8 { + return mapScaleFactor +} + +// IP address of the interface through which the agent traffic is routed +func IPAddress() string { + return ipAddress +} + +// Hostname of the machine that is running the agent +func Hostname() string { + return hostname +} + +// Kernel release (uname -r) of the machine that is running the agent +func KernelVersion() string { + return kernelVersion +} + +// Threshold for probabilistic profiling +func ProbabilisticThreshold() uint { + return probabilisticThreshold +} + +// Number of CPU cores +func PresentCPUCores() uint16 { + return presentCPUCores +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..aa242d82 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,63 @@ +/* + * 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 config + +import ( + "os" + "testing" +) + +func TestSetConfiguration(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current working directory: %v", err) + } + + cfg := Config{ + ProjectID: 42, + CacheDirectory: cwd, + EnvironmentType: "aws", + MachineID: "0xfeeddeadbeefbeef", + SecretToken: "secret", + ValidatedTags: "", + } + + // Test setting environment to "aws". + err = SetConfiguration(&cfg) + if err != nil { + t.Fatalf("failure to set environment to \"aws\"") + } + + cfg2 := cfg + cfg2.EnvironmentType = "bla" + err = SetConfiguration(&cfg2) + if err == nil { + t.Fatalf("expected failure using invalid environment (%s)", err) + } + + cfg3 := cfg + cfg3.MachineID = "" + err = SetConfiguration(&cfg3) + if err == nil { + t.Fatalf("expected failure using empty machineID for environment (%s)", err) + } + + cfg4 := cfg + cfg4.EnvironmentType = "" + err = SetConfiguration(&cfg4) + if err == nil { + t.Fatalf("expected failure using empty environment for machineID (%s)", err) + } + + cfg5 := cfg + cfg5.EnvironmentType = "aws" + cfg5.SecretToken = "" + err = SetConfiguration(&cfg5) + if err == nil { + t.Fatalf("expected failure using empty secretToken for environment") + } +} diff --git a/config/env.go b/config/env.go new file mode 100644 index 00000000..4188911c --- /dev/null +++ b/config/env.go @@ -0,0 +1,492 @@ +/* + * 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 config + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "runtime" + "sort" + "strings" + "sync" + "time" + + "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" + 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. +type EnvironmentType uint8 + +// The EnvironmentType part in hostID is 0xF. So values should not exceed this limit. +const ( + envUnspec EnvironmentType = iota // envUnspec indicates we can't identify the environment + envHardware + envLXC + envKVM + envDocker + envGCP + envAzure + envAWS +) + +// environment indicates the cloud/virtualization environment in which the agent is running. It +// can be set in the configuration file, or it will be automatically determined. It is used in +// the computation of the host ID. If it is specified in the configuration file then the machine +// ID must also be specified. +var environment = envUnspec + +// machineID specifies a unique identifier for the host on which the agent is running. It can be +// set in the configuration file, or it will be automatically determined. It is used in the +// computation of the host ID. If it is specified in the configuration file then the environment +// must also be specified. +var machineID uint64 + +func (e EnvironmentType) String() string { + switch e { + case envUnspec: + return "unspecified" + case envHardware: + return "hardware" + case envLXC: + return "lxc" + case envKVM: + return "kvm" + case envDocker: + return "docker" + case envGCP: + return "gcp" + case envAzure: + return "azure" + case envAWS: + // nolint: goconst + return "aws" + default: + return fmt.Sprintf("unknown environment %d", e) + } +} + +// RunsOnGCP returns true if host agent runs on GCP. +func RunsOnGCP() bool { + return environment == envGCP +} + +// RunsOnAzure returns true if host agent runs on Azure. +func RunsOnAzure() bool { + return environment == envAzure +} + +// RunsOnAWS returns true if host agent runs on AWS. +func RunsOnAWS() bool { + return environment == envAWS +} + +var strToEnv = map[string]EnvironmentType{ + "hardware": envHardware, + "lxc": envLXC, + "kvm": envKVM, + "docker": envDocker, + "gcp": envGCP, + "azure": envAzure, + "aws": envAWS, +} + +// environmentTypeFromString converts a string to an environment specifier. The matching is case +// insensitive. +func environmentTypeFromString(envStr string) (EnvironmentType, error) { + if env, ok := strToEnv[strings.ToLower(envStr)]; ok { + return env, nil + } + + return envUnspec, fmt.Errorf("unknown environment type: %s", envStr) +} + +// readFile reads in a given file and returns its content as a string +func readFile(file string) (string, error) { + bytes, err := os.ReadFile(file) + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", file, err) + } + return string(bytes), nil +} + +// CheckCgroups is used to detect if we are running containerized in docker, lxc, or kvm. +func checkCGroups() (EnvironmentType, error) { + data, err := readFile("/proc/1/cgroup") + if err != nil { + return envUnspec, fmt.Errorf("failed to read /proc/1/cgroup: %s", err) + } + + if strings.Contains(data, "docker") { + return envDocker, nil + } else if strings.Contains(data, "lxc") { + return envLXC, nil + } else if strings.Contains(data, "kvm") { + return envKVM, nil + } + + return envHardware, nil +} + +// getFamily returns the address family of the given IP. +func getFamily(ip net.IP) uint8 { + if ip.To4() != nil { + return unix.AF_INET + } + + return unix.AF_INET6 +} + +func getInterfaceIndexFromIP(conn *rtnetlink.Conn, ip net.IP) (index int, err error) { + routeMsg := rtnetlink.RouteMessage{ + Attributes: rtnetlink.RouteAttributes{ + Dst: ip, + }, + Family: getFamily(ip), + } + + msgs, err := conn.Route.Get(&routeMsg) + if err != nil { + return -1, fmt.Errorf("failed to get route: %s", err) + } + if len(msgs) == 0 { + return -1, fmt.Errorf("empty routing table") + } + + return int(msgs[0].Attributes.OutIface), nil +} + +func hwAddrToUint64(hwAddr net.HardwareAddr) uint64 { + if len(hwAddr) < 8 { + hwAddr = append(hwAddr, make(net.HardwareAddr, 8-len(hwAddr))...) + } + return binary.LittleEndian.Uint64(hwAddr) +} + +// getMACFromRouting returns the MAC address of network interface of CA traffic routing. +func getMACFromRouting(destination string) (mac uint64, err error) { + addrs, err := net.LookupIP(destination) + if err != nil { + 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") + } + + // 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") + } + defer conn.Close() + + seenIfaces := make(libpf.Set[int]) + for _, ip := range addrs { + ifaceIndex, err := getInterfaceIndexFromIP(conn, ip) + if err != nil { + log.Warnf("Failed to get interface index for %s: %v", ip, err) + continue + } + + if _, ok := seenIfaces[ifaceIndex]; ok { + continue + } + seenIfaces[ifaceIndex] = libpf.Void{} + + iface, err := net.InterfaceByIndex(ifaceIndex) + if err != nil { + log.Warnf("Failed to get outgoing interface for %s: %v", ip, err) + continue + } + + hwAddr := iface.HardwareAddr + if len(hwAddr) == 0 { + continue + } + + return hwAddrToUint64(hwAddr), nil + } + + return 0, fmt.Errorf("failed to retrieve MAC from routing interface") +} + +// getMACFromSystem returns a MAC address by iterating over all system +// network interfaces (in increasing ifindex order therefore prioritizing physical +// interfaces) and selecting an address belonging to a non-loopback interface. +func getMACFromSystem() (mac uint64, err error) { + ifaces, err := net.Interfaces() + if err != nil { + return 0, err + } + + // The reason for sorting by ifindex here, is that it doesn't change when + // an interface is set to up/down. Therefore by prioritizing interfaces in + // increasing ifindex order, we're prioritizing physical/hardware + // interfaces (since the ifindex for these is usually assigned at boot, + // while temporary/tunnel interfaces are usually created later, post + // system-networking-is-up). + + // Don't rely on net.Interfaces/RTM_GETLINK ifindex sorting + sort.SliceStable(ifaces, func(i, j int) bool { + return ifaces[i].Index < ifaces[j].Index + }) + + macs := make([]uint64, 0, len(ifaces)) + for _, iface := range ifaces { + if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) == 0 { + continue + } + + hwAddr := iface.HardwareAddr + if iface.Flags&net.FlagUp != 0 { + // Return the MAC address belonging to the first non-loopback + // network interface encountered that is UP. + return hwAddrToUint64(hwAddr), nil + } + macs = append(macs, hwAddrToUint64(hwAddr)) + } + + if len(macs) > 0 { + // Since no usable MAC addresses belonging to network interfaces + // that are UP were found, return an address from a network interface + // that is not UP. + return macs[0], nil + } + + return 0, fmt.Errorf("failed to find a MAC") +} + +// getNonCloudEnvironmentAndMachineID tries to detect if the agent is running in a +// virtualized environment or on hardware. It returns a machineID and a environment +// specific identifier. +// TODO(PF-1007): move to a random ID (possibly persisted on the filesystem). +func getNonCloudEnvironmentAndMachineID() (uint64, EnvironmentType, error) { + var env EnvironmentType + var err error + var id, mac uint64 + + if env, err = checkCGroups(); err != nil { + return 0, env, err + } + + if id, err = getMachineID(); err != nil { + return 0, env, err + } + + // Cloned VMs or container images might have the same machine ID. + // We add an XOR of the MAC addresses to get a unique ID. + // Extract the MAC address from the root network namespace, because the MAC address visible in + // some containerized environments may not be enough to guarantee unicity. + // We need to do this from a dedicated thread to avoid affecting other threads + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + // LockOSThread ensures the thread exits after the goroutine exits, avoiding namespace + // leakage in other goroutines. + runtime.LockOSThread() + var ns int + ns, err = pfnamespaces.EnterNamespace(1, "net") + if err != nil { + err = fmt.Errorf("unable to enter PID 1 network namespace: %v", err) + return + } + defer unix.Close(ns) + + mac, err = getMACFromRouting("example.com") + if err != nil { + log.Warnf("%v", err) + mac, err = getMACFromSystem() + } + }() + wg.Wait() + if err != nil { + return 0, env, err + } + + log.Debugf("Using MAC: 0x%X", mac) + id ^= mac + + return id, env, err +} + +// idFromString generates a number, that will be part of the hostID, from a given string. +func idFromString(s string) uint64 { + return libpf.HashString(s) +} + +// gcpInfo collects information about the GCP environment +// https://cloud.google.com/compute/docs/storing-retrieving-metadata +func gcpInfo() (uint64, EnvironmentType, error) { + instanceID, err := gcp.InstanceID() + if err != nil { + return 0, envGCP, fmt.Errorf("failed to get GCP metadata: %w", err) + } + return idFromString(instanceID), envGCP, nil +} + +// awsInfo collects information about the AWS environment +func awsInfo() (uint64, EnvironmentType, error) { + svc := awsec2.New(awssession.Must(awssession.NewSession())) + document, err := svc.GetInstanceIdentityDocument() + if err != nil { + return 0, envAWS, fmt.Errorf("failed to get aws metadata: %w", err) + } + return idFromString(document.InstanceID), envAWS, nil +} + +// AzureInstanceMetadata as provided by +// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service +// It is needed to decode the json encoded return by Azure. +type AzureInstanceMetadata struct { + Location string `json:"location"` + Name string `json:"name"` + SubscriptionID string `json:"subscriptionId"` + Tags string `json:"tags"` + Version string `json:"version"` + VMID string `json:"vmId"` + Zone string `json:"zone"` +} + +// azureInfo collects information about the Azure environment +func azureInfo() (uint64, EnvironmentType, error) { + var m AzureInstanceMetadata + c := &http.Client{Timeout: 3 * time.Second} + + req, _ := http.NewRequest(http.MethodGet, "http://169.254.169.254/metadata/instance/compute", + http.NoBody) + req.Header.Add("Metadata", "True") + q := req.URL.Query() + q.Add("format", "json") + q.Add("api-version", "2020-09-01") + req.URL.RawQuery = q.Encode() + + resp, err := c.Do(req) + if err != nil { + return 0, envAzure, fmt.Errorf("failed to get azure metadata: %s", err) + } + defer resp.Body.Close() + + rawJSON, err := io.ReadAll(resp.Body) + if err != nil { + return 0, envAzure, fmt.Errorf("failed to read azure response: %s", err) + } + if err = json.Unmarshal(rawJSON, &m); err != nil { + return 0, envAzure, fmt.Errorf("failed to unmarshal JSON response: %s", err) + } + return idFromString(m.VMID), envAzure, nil +} + +// getMachineID returns the id according to +// http://man7.org/linux/man-pages/man5/machine-id.5.html +func getMachineID() (uint64, error) { + var id uint64 = 42 + var err error + var data string + + for _, file := range []string{"/var/lib/dbus/machine-id", "/etc/machine-id"} { + data, err = readFile(file) + if err != nil { + continue + } + return idFromString(data), nil + } + return id, err +} + +// environmentTester represents a function, which returns a unique identifier for this environment, +// an environment specific EnvironmentType or an error otherwise. +type environmentTester func() (uint64, EnvironmentType, error) + +func getEnvironmentAndMachineID() (EnvironmentType, uint64, error) { + var env EnvironmentType + // environmentTests is a list of functions, that can be used to check the environment. + // The order of the list matters. So gcpInfo will be called first, followed by + // awsInfo and azureInfo. + environmentTests := []environmentTester{gcpInfo, awsInfo, azureInfo} + + var wg sync.WaitGroup + for _, envTest := range environmentTests { + wg.Add(1) + go func(testEnv environmentTester) { + defer wg.Done() + mID, envT, err := testEnv() + if err != nil { + log.Debugf("Environment tester (%s) failed: %s", envT, err) + return + } + machineID = mID + env = envT + }(envTest) + } + wg.Wait() + + if env == envUnspec { + var err error + // the environment check getNonCloudEnvironmentAndMachineID is not part of + // environmentTests, as it is our last resort in identifiying the environment. + machineID, env, err = getNonCloudEnvironmentAndMachineID() + if env == envUnspec || err != nil { + return envUnspec, 0, fmt.Errorf( + "failed to determine environment and machine ID: %s", err) + } + } + + return env, machineID, nil +} + +func setEnvironment(env EnvironmentType) { + environment = env +} + +// GenerateHostID generates and sets the unique hostID +func GenerateNewHostIDIfNecessary() error { + var err error + + if hostID != 0 { + log.Info("HostID is already set, returning without doing anything.") + return nil + } + + if environment == envUnspec { + log.Info("Automatically determining environment and machine ID ...") + environment, machineID, err = getEnvironmentAndMachineID() + if err != nil { + return err + } + log.Infof("Environment: %s, machine ID: 0x%x", environment, machineID) + } else { + log.Infof("Using provided environment (%s) and machine ID (0x%x)", environment, + machineID) + } + + // set the package wide available variable + // hostID is declared in config.go + + // Parts of the hostID: + // 0xf000000000000000 - environment identifier + // 0x0fffffffffffffff - machine id + + hostID |= (uint64(environment&0xf) << 60) + hostID |= (machineID & 0x0fffffffffffffff) + + return nil +} diff --git a/config/times.go b/config/times.go new file mode 100644 index 00000000..0118e1d1 --- /dev/null +++ b/config/times.go @@ -0,0 +1,104 @@ +/* + * 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 config + +import ( + "time" + + log "github.com/sirupsen/logrus" +) + +var times = Times{ + reportMetricsInterval: 1 * time.Minute, + // GRPCAuthErrorDelay defines the delay before triggering a global process exit after a + // gRPC auth error. + grpcAuthErrorDelay: 10 * time.Minute, + // GRPCConnectionTimeout defines the timeout for each established gRPC connection. + grpcConnectionTimeout: 3 * time.Second, + // GRPCOperationTimeout defines the timeout for each gRPC operation. + grpcOperationTimeout: 5 * time.Second, + // GRPCStartupBackoffTimeout defines the time between failed gRPC requests during startup + // phase. + grpcStartupBackoffTimeout: 1 * time.Minute, + pidCleanupInterval: 5 * time.Minute, + tracePollInterval: 250 * time.Millisecond, +} + +// Compile time check for interface adherence +var _ IntervalsAndTimers = (*Times)(nil) + +// Times hold all the intervals and timeouts that are used across the host agent in a central place +// and comes with Getters to read them. +type Times struct { + monitorInterval time.Duration + tracePollInterval time.Duration + reportInterval time.Duration + reportMetricsInterval time.Duration + grpcConnectionTimeout time.Duration + grpcOperationTimeout time.Duration + grpcStartupBackoffTimeout time.Duration + grpcAuthErrorDelay time.Duration + pidCleanupInterval time.Duration + probabilisticInterval time.Duration +} + +// IntervalsAndTimers is a meta interface that exists purely to document its functionality. +type IntervalsAndTimers interface { + // MonitorInterval defines the interval for PID event monitoring and metric collection. + MonitorInterval() time.Duration + // TracePollInterval defines the interval at which we read the trace perf event buffer. + TracePollInterval() time.Duration + // ReportInterval defines the interval at which collected data is sent to collection agent. + ReportInterval() time.Duration + // ReportMetricsInterval defines the interval at which collected metrics are sent + // to collection agent. + ReportMetricsInterval() time.Duration + // GRPCConnectionTimeout defines the timeout for each established gRPC connection. + GRPCConnectionTimeout() time.Duration + // GRPCOperationTimeout defines the timeout for each gRPC operation. + GRPCOperationTimeout() time.Duration + // GRPCStartupBackoffTime defines the time between failed gRPC requests during startup + // phase. + GRPCStartupBackoffTime() time.Duration + // GRPCAuthErrorDelay defines the delay before triggering a global process exit after a + // gRPC auth error. + GRPCAuthErrorDelay() time.Duration + // PIDCleanupInterval defines the interval at which monitored PIDs are checked for + // liveness and no longer alive PIDs are cleaned up. + PIDCleanupInterval() time.Duration + // ProbabilisticInterval defines the interval for which probabilistic profiling will + // be enabled or disabled. + ProbabilisticInterval() time.Duration +} + +func (t *Times) MonitorInterval() time.Duration { return t.monitorInterval } + +func (t *Times) TracePollInterval() time.Duration { return t.tracePollInterval } + +func (t *Times) ReportInterval() time.Duration { return t.reportInterval } + +func (t *Times) ReportMetricsInterval() time.Duration { return t.reportMetricsInterval } + +func (t *Times) GRPCConnectionTimeout() time.Duration { return t.grpcConnectionTimeout } + +func (t *Times) GRPCOperationTimeout() time.Duration { return t.grpcOperationTimeout } + +func (t *Times) GRPCStartupBackoffTime() time.Duration { return t.grpcStartupBackoffTimeout } + +func (t *Times) GRPCAuthErrorDelay() time.Duration { return t.grpcAuthErrorDelay } + +func (t *Times) PIDCleanupInterval() time.Duration { return t.pidCleanupInterval } + +func (t *Times) ProbabilisticInterval() time.Duration { return t.probabilisticInterval } + +// GetTimes provides access to all timers and intervals. +func GetTimes() *Times { + if !configurationSet { + log.Fatal("Cannot get Times. Configuration has not been read") + } + return × +} diff --git a/config/tracer.go b/config/tracer.go new file mode 100644 index 00000000..d9141ec6 --- /dev/null +++ b/config/tracer.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 config + +// TracerType values identify tracers, such as the native code tracer, or PHP tracer +type TracerType int + +const ( + PerlTracer TracerType = iota + PHPTracer + PythonTracer + HotspotTracer + RubyTracer + V8Tracer + + // MaxTracers indicates the max. number of different tracers + MaxTracers +) + +var tracerTypeToString = map[TracerType]string{ + PerlTracer: "perl", + PHPTracer: "php", + PythonTracer: "python", + HotspotTracer: "hotspot", + RubyTracer: "ruby", + V8Tracer: "v8", +} + +// 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 + +// 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) + } + + for i := 0; i < int(MaxTracers); i++ { + if allTracers[i] != TracerType(i) { + allTracers[i] = TracerType(i) + } + } + return allTracers +} + +// GetString converts the tracer type t to its related string value. +func (t TracerType) GetString() string { + if result, ok := tracerTypeToString[t]; ok { + return result + } + + return "" +} diff --git a/containermetadata/containermetadata.go b/containermetadata/containermetadata.go new file mode 100644 index 00000000..8f8f096f --- /dev/null +++ b/containermetadata/containermetadata.go @@ -0,0 +1,713 @@ +/* + * 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. + */ + +// containermetadata provides functionality for retrieving the kubernetes pod and container +// metadata or the docker container metadata for a particular PID. +// For kubernetes it uses the shared informer from the k8s client-go API +// (https://github.com/kubernetes/client-go/blob/master/tools/cache/shared_informer.go). Through +// the shared informer we are notified of changes in the state of pods in the Kubernetes +// cluster and can add the pod container metadata to the cache. +// As a backup to the kubernetes shared informer and to find the docker container metadata for +// each pid received (if it is not already in the container caches), it will retrieve the container +// id from the /proc/PID/cgroup and retrieve the metadata for the containerID. +package containermetadata + +import ( + "bufio" + "context" + "errors" + "fmt" + "os" + "regexp" + "strings" + "sync/atomic" + "time" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/namespaces" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + lru "github.com/elastic/go-freelru" + log "github.com/sirupsen/logrus" + "github.com/zeebo/xxh3" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/periodiccaller" + "github.com/elastic/otel-profiling-agent/libpf/stringutil" + "github.com/elastic/otel-profiling-agent/metrics" +) + +const ( + dockerHost = "DOCKER_HOST" + kubernetesServiceHost = "KUBERNETES_SERVICE_HOST" + kubernetesNodeName = "KUBERNETES_NODE_NAME" + genericNodeName = "NODE_NAME" + + // There is a limit of 110 Pods per node (but can be overridden) + kubernetesPodsPerNode = 110 + // From experience, usually there are no more than 10 containers (including sidecar + // containers) in a single Pod. + kubernetesContainersPerPod = 10 + // We're setting the default cache size according to Kubernetes best practices, + // in order to reduce the number of Kubernetes API calls at runtime. + containerMetadataCacheSize = kubernetesPodsPerNode * kubernetesContainersPerPod + + // containerIDCacheSize defines the size of the cache which maps a process to container ID + // information. Its perfect size would be the number of processes running on the system. + containerIDCacheSize = 1024 +) + +var ( + kubePattern = regexp.MustCompile(`\d+:.*:/.*/*kubepods/[^/]+/pod[^/]+/([0-9a-f]{64})`) + dockerKubePattern = regexp.MustCompile(`\d+:.*:/.*/*docker/pod[^/]+/([0-9a-f]{64})`) + altKubePattern = regexp.MustCompile( + `\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})`) + 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_-]+)`) + + containerIDPattern = regexp.MustCompile(`.+://([0-9a-f]{64})`) + + cgroup = "/proc/%d/cgroup" +) + +// 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 + containerdClientQueryCount atomic.Uint64 + + // the kubernetes node name used to retrieve the pod information. + nodeName string + // containerMetadataCache provides a cache to quickly retrieve the pod metadata for a + // particular container id. It caches the pod name and container name metadata. Locked LRU. + containerMetadataCache *lru.SyncedLRU[string, ContainerMetadata] + + // containerIDCache stores per process container ID information. + containerIDCache *lru.SyncedLRU[libpf.OnDiskFileIdentifier, containerIDEntry] + + kubeClientSet kubernetes.Interface + dockerClient *client.Client + + containerdClient *containerd.Client +} + +// ContainerMetadata contains the container and/or pod metadata. +type ContainerMetadata struct { + containerID string + PodName string + ContainerName string +} + +// hashString is a helper function for containerMetadataCache +// 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)) +} + +// containerEnvironment specifies a used container technology. +type containerEnvironment uint16 + +// List of known container technologies we can handle. +const ( + envUndefined containerEnvironment = 0 + envKubernetes containerEnvironment = 1 << iota + envDocker + envLxc + envContainerd + envDockerBuildkit +) + +// isContainerEnvironment tests if env is target. +func isContainerEnvironment(env, target containerEnvironment) bool { + return target&env == target +} + +// containerIDEntry stores the information we fetch from the cgroup information of the process. +type containerIDEntry struct { + containerID string + env containerEnvironment +} + +// 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) + if err != nil { + return nil, fmt.Errorf("unable to create container id cache: %v", err) + } + + instance := &Handler{ + containerIDCache: containerIDCache, + dockerClient: getDockerClient(), + containerdClient: getContainerdClient(), + } + + if os.Getenv(kubernetesServiceHost) != "" { + err = createKubernetesClient(ctx, instance) + if err != nil { + return nil, fmt.Errorf("failed to create kubernetes client %v", err) + } + } else { + log.Infof("Environment variable %s not set", kubernetesServiceHost) + instance.containerMetadataCache, err = lru.NewSynced[string, ContainerMetadata]( + containerMetadataCacheSize, hashString) + if err != nil { + return nil, fmt.Errorf("unable to create container metadata cache: %v", err) + } + } + + log.Debugf("Container metadata handler: %v", instance) + + periodiccaller.Start(ctx, monitorInterval, func() { + metrics.AddSlice([]metrics.Metric{ + { + ID: metrics.IDKubernetesClientQuery, + Value: metrics.MetricValue( + instance.kubernetesClientQueryCount.Swap(0)), + }, + { + ID: metrics.IDDockerClientQuery, + Value: metrics.MetricValue( + instance.dockerClientQueryCount.Swap(0)), + }, + { + ID: metrics.IDContainerdClientQuery, + Value: metrics.MetricValue( + instance.containerdClientQueryCount.Swap(0)), + }, + }) + }) + + return instance, nil +} + +// 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) { + instance.kubernetesClientQueryCount.Add(1) + nodeList, err := instance.kubeClientSet.CoreV1().Nodes().List(ctx, v1.ListOptions{ + FieldSelector: "spec.nodeName=" + instance.nodeName, + }) + if err != nil { + return 0, fmt.Errorf("failed to get kubernetes nodes for '%s': %v", + instance.nodeName, err) + } + + if len(nodeList.Items) == 0 { + return 0, fmt.Errorf("empty node list") + } + + // With the ListOptions filter in place, there should be only one node listed in the + // return we get from the API. + quantity, ok := nodeList.Items[0].Status.Allocatable[corev1.ResourcePods] + if !ok { + return 0, fmt.Errorf("failed to get allocatable information from %s", + nodeList.Items[0].Name) + } + + return int(quantity.Value()), nil +} + +func getContainerMetadataCache(ctx context.Context, instance *Handler) ( + *lru.SyncedLRU[string, ContainerMetadata], error) { + cacheSize := containerMetadataCacheSize + + podsPerNode, err := getPodsPerNode(ctx, instance) + if err != nil { + log.Infof("Failed to get pods per node: %v", err) + } else { + cacheSize *= podsPerNode + } + + return lru.NewSynced[string, ContainerMetadata]( + uint32(cacheSize), hashString) +} + +func createKubernetesClient(ctx context.Context, instance *Handler) error { + log.Debugf("Create Kubernetes client") + + config, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("failed to create in cluster configuration for Kubernetes: %v", err) + } + instance.kubeClientSet, err = kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %v", err) + } + + k, ok := instance.kubeClientSet.(*kubernetes.Clientset) + if !ok { + return fmt.Errorf("failed to create Kubernetes client: %v", err) + } + + instance.nodeName, err = getNodeName() + if err != nil { + return fmt.Errorf("failed to get kubernetes node name; %v", err) + } + + instance.containerMetadataCache, err = getContainerMetadataCache(ctx, instance) + if err != nil { + return fmt.Errorf("failed to create container metadata cache: %v", err) + } + + // Create the shared informer factory and use the client to connect to + // Kubernetes and get notified of new pods that are created in the specified node. + factory := informers.NewSharedInformerFactoryWithOptions(k, 0, + informers.WithTweakListOptions(func(options *v1.ListOptions) { + options.FieldSelector = "spec.nodeName=" + instance.nodeName + })) + informer := factory.Core().V1().Pods().Informer() + + // Kubernetes serves a utility to handle API crashes + defer runtime.HandleCrash() + + handle, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj any) { + pod, ok := obj.(*corev1.Pod) + if !ok { + log.Errorf("Received unknown object in AddFunc handler: %#v", obj) + return + } + instance.putCache(pod) + }, + UpdateFunc: func(oldObj any, newObj any) { + pod, ok := newObj.(*corev1.Pod) + if !ok { + log.Errorf("Received unknown object in UpdateFunc handler: %#v", + newObj) + return + } + instance.putCache(pod) + }, + }) + if err != nil { + return fmt.Errorf("failed to attach event handler: %v", err) + } + + // Shutdown the informer when the context attached to this Handler expires + stopper := make(chan struct{}) + go func() { + <-ctx.Done() + close(stopper) + if err := informer.RemoveEventHandler(handle); err != nil { + log.Errorf("Failed to remove event handler: %v", err) + } + }() + // Run the informer + go informer.Run(stopper) + + return nil +} + +func getContainerdClient() *containerd.Client { + knownContainerdSockets := []string{"/run/containerd/containerd.sock", + "/var/run/containerd/containerd.sock", + "/var/run/docker/containerd/containerd.sock"} + + for _, socket := range knownContainerdSockets { + if _, err := os.Stat(socket); err != nil { + continue + } + opt := containerd.WithTimeout(3 * time.Second) + if c, err := containerd.New(socket, opt); err == nil { + return c + } + } + log.Infof("Can't connect Containerd client to %v", knownContainerdSockets) + return nil +} + +func getDockerClient() *client.Client { + // /var/run/docker.sock is the default socket used by client.NewEnvClient(). + knownDockerSockets := []string{"/var/run/docker.sock"} + + // If the default socket is not available check if DOCKER_HOST is set to a different socket. + envDockerSocket := os.Getenv(dockerHost) + if envDockerSocket != "" { + knownDockerSockets = append(knownDockerSockets, envDockerSocket) + } + + for _, socket := range knownDockerSockets { + if _, err := os.Stat(socket); err != nil { + continue + } + if c, err := client.NewClientWithOpts( + client.FromEnv, + client.WithAPIVersionNegotiation(), + ); err == nil { + return c + } + } + log.Infof("Can't connect Docker client to %v", knownDockerSockets) + return nil +} + +// putCache updates the container id metadata cache for the provided pod. +func (h *Handler) putCache(pod *corev1.Pod) { + log.Debugf("Update container metadata cache for pod %s", pod.Name) + podName := getPodName(pod) + + for i := range pod.Status.ContainerStatuses { + var containerID string + var err error + if containerID, err = matchContainerID( + pod.Status.ContainerStatuses[i].ContainerID); err != nil { + log.Debugf("failed to get kubernetes container metadata: %v", err) + continue + } + + h.containerMetadataCache.Add(containerID, ContainerMetadata{ + containerID: containerID, + PodName: podName, + ContainerName: pod.Status.ContainerStatuses[i].Name, + }) + } +} + +func getPodName(pod *corev1.Pod) string { + podName := pod.Name + + for j := range pod.OwnerReferences { + if strings.HasPrefix(podName, pod.OwnerReferences[j].Name) { + switch pod.OwnerReferences[j].Kind { + case "ReplicaSet": + // For replicaSet the Owner references Name contains the replicaset version + // ie 'deployment-replicaset' which we want to remove. + lastIndex := strings.LastIndex(pod.OwnerReferences[j].Name, "-") + if lastIndex < 0 { + // pod.OwnerReferences[j].Name does not contain a '-' so + // we take the full name as PodName and avoid to panic. + podName = pod.OwnerReferences[j].Name + } else { + podName = pod.OwnerReferences[j].Name[:lastIndex] + } + default: + podName = pod.OwnerReferences[j].Name + } + } + } + + return podName +} + +func matchContainerID(containerIDStr string) (string, error) { + containerIDParts := containerIDPattern.FindStringSubmatch(containerIDStr) + if len(containerIDParts) != 2 { + return "", fmt.Errorf("could not get string submatch for container id %v", + containerIDStr) + } + return containerIDParts[1], nil +} + +func getNodeName() (string, error) { + nodeName := os.Getenv(kubernetesNodeName) + if nodeName != "" { + return nodeName, nil + } + log.Debugf("%s not set", kubernetesNodeName) + + // The Elastic manifest for kubernetes uses NODE_NAME instead of KUBERNETES_NODE_NAME. + // Therefore, we check for both environment variables. + nodeName = os.Getenv(genericNodeName) + if nodeName == "" { + return "", fmt.Errorf("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) { + // 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. + pidContainerID, env, err := h.lookupContainerID(pid) + if err != nil { + return ContainerMetadata{}, fmt.Errorf("failed to get container id for pid %d", pid) + } + if envUndefined == env { + // We were not able to identify a container technology for the given PID. + return ContainerMetadata{}, nil + } + + // Fast path, check if the containerID metadata has been cached + if data, ok := h.containerMetadataCache.Get(pidContainerID); ok { + return data, nil + } + + // For kubernetes pods this route should happen rarely, this means that we are processing a + // 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 { + return h.getKubernetesPodMetadata(pidContainerID) + } else if isContainerEnvironment(env, envDocker) && h.dockerClient != nil { + return h.getDockerContainerMetadata(pidContainerID) + } else if isContainerEnvironment(env, envContainerd) && h.containerdClient != nil { + return h.getContainerdContainerMetadata(pidContainerID) + } else if 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. + return ContainerMetadata{ + containerID: pidContainerID, + ContainerName: pidContainerID, + }, nil + } else if 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 + } + + return ContainerMetadata{}, fmt.Errorf("failed to handle unknown container technology %d", env) +} + +func (h *Handler) getKubernetesPodMetadata(pidContainerID string) ( + ContainerMetadata, error) { + log.Debugf("Get kubernetes pod metadata for container id %v", pidContainerID) + + h.kubernetesClientQueryCount.Add(1) + pods, err := h.kubeClientSet.CoreV1().Pods("").List(context.TODO(), v1.ListOptions{ + FieldSelector: "spec.nodeName=" + h.nodeName, + }) + if err != nil { + return ContainerMetadata{}, fmt.Errorf("failed to retrieve kubernetes pods, %v", err) + } + + for j := range pods.Items { + podName := getPodName(&pods.Items[j]) + containers := pods.Items[j].Status.ContainerStatuses + for i := range containers { + var containerID string + if containers[i].ContainerID == "" { + continue + } + if containerID, err = matchContainerID(containers[i].ContainerID); err != nil { + log.Error(err) + continue + } + if containerID == pidContainerID { + containerMetadata := ContainerMetadata{ + containerID: containerID, + PodName: podName, + ContainerName: containers[i].Name, + } + h.containerMetadataCache.Add(containerID, containerMetadata) + + return containerMetadata, nil + } + } + } + + return ContainerMetadata{}, + fmt.Errorf("failed to find matching kubernetes pod/container metadata for "+ + "containerID '%v' in %d pods", pidContainerID, len(pods.Items)) +} + +func (h *Handler) getDockerContainerMetadata(pidContainerID string) ( + ContainerMetadata, error) { + log.Debugf("Get docker container metadata for container id %v", pidContainerID) + + h.dockerClientQueryCount.Add(1) + containers, err := h.dockerClient.ContainerList(context.Background(), + container.ListOptions{}) + if err != nil { + return ContainerMetadata{}, fmt.Errorf("failed to list docker containers, %v", err) + } + + for i := range containers { + if containers[i].ID == pidContainerID { + // remove / prefix from container name + containerName := strings.TrimPrefix(containers[i].Names[0], "/") + metadata := ContainerMetadata{ + containerID: containers[i].ID, + ContainerName: containerName, + } + h.containerMetadataCache.Add(pidContainerID, metadata) + return metadata, nil + } + } + + return ContainerMetadata{}, + fmt.Errorf("failed to find matching container metadata for containerID, %v", + pidContainerID) +} + +func (h *Handler) getContainerdContainerMetadata(pidContainerID string) ( + ContainerMetadata, error) { + log.Debugf("Get containerd container metadata for container id %v", pidContainerID) + + // Avoid heap allocations here - do not use strings.SplitN() + var fields [4]string // allocate the array on the stack with capacity 3 + n := stringutil.SplitN(pidContainerID, "/", fields[:]) + + if n < 3 { + return ContainerMetadata{}, + fmt.Errorf("unexpected format of containerd identifier: %s", + pidContainerID) + } + + h.containerdClientQueryCount.Add(1) + ctx := namespaces.WithNamespace(context.Background(), fields[1]) + containers, err := h.containerdClient.Containers(ctx) + if err != nil { + return ContainerMetadata{}, + fmt.Errorf("failed to get containerd containers in namespace '%s': %v", + fields[1], err) + } + + for _, container := range containers { + if container.ID() == fields[2] { + // Containerd does not differentiate between the name and the ID of a + // container. So we both options to the same value. + return ContainerMetadata{ + containerID: fields[2], + ContainerName: fields[2], + PodName: fields[1], + }, nil + } + } + + return ContainerMetadata{}, + fmt.Errorf("failed to find matching 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, + err error) { + cgroupFilePath := fmt.Sprintf(cgroup, pid) + + fileIdentifier, err := libpf.GetOnDiskFileIdentifier(cgroupFilePath) + if err != nil { + return "", envUndefined, nil + } + + if entry, exists := h.containerIDCache.Get(fileIdentifier); exists { + return entry.containerID, entry.env, nil + } + + containerID, env, err = h.extractContainerIDFromFile(cgroupFilePath) + if err != nil { + return "", envUndefined, err + } + + // Store the result in the cache. + h.containerIDCache.Add(fileIdentifier, containerIDEntry{ + containerID: containerID, + env: env, + }) + + return containerID, env, nil +} + +func (h *Handler) extractContainerIDFromFile(cgroupFilePath string) ( + containerID string, env containerEnvironment, err error) { + f, err := os.Open(cgroupFilePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + log.Debugf("%s does not exist anymore. "+ + "Failed to get container id", cgroupFilePath) + return "", envUndefined, nil + } + return "", envUndefined, fmt.Errorf("failed to get container id from %s: %v", + cgroupFilePath, err) + } + defer f.Close() + + containerID = "" + env = envUndefined + + scanner := bufio.NewScanner(f) + buf := make([]byte, 512) + // Providing a predefined buffer overrides the internal buffer that Scanner uses (4096 bytes). + // We can do that and also set a maximum allocation size on the following call. + // With a maximum of 4096 characters path in the kernel, 8192 should be fine here. We don't + // expect lines in /proc//cgroup to be longer than that. + scanner.Buffer(buf, 8192) + + var parts []string + for scanner.Scan() { + line := scanner.Text() + + if h.kubeClientSet != nil { + parts = dockerKubePattern.FindStringSubmatch(line) + if parts != nil { + containerID = parts[1] + env |= (envKubernetes | envDocker) + break + } + parts = kubePattern.FindStringSubmatch(line) + if parts != nil { + containerID = parts[1] + env |= envKubernetes + break + } + parts = altKubePattern.FindStringSubmatch(line) + if parts != nil { + containerID = parts[1] + env |= envKubernetes + break + } + parts = systemdKubePattern.FindStringSubmatch(line) + if parts != nil { + containerID = parts[1] + env |= envKubernetes + break + } + } + + if h.dockerClient != nil { + if parts = dockerPattern.FindStringSubmatch(line); parts != nil { + containerID = parts[1] + env |= envDocker + break + } + if parts = dockerBuildkitPattern.FindStringSubmatch(line); parts != nil { + containerID = parts[1] + env |= envDockerBuildkit + break + } + } + + if h.containerdClient != nil { + if parts = containerdPattern.FindStringSubmatch(line); parts != nil { + // Forward the complete match as containerID so, we can extract later + // the exact containerd namespace and container ID from it. + containerID = parts[0] + env |= envContainerd + break + } + } + + if parts = lxcPattern.FindStringSubmatch(line); parts != nil { + containerID = parts[2] + env |= envLxc + break + } + } + + return containerID, env, nil +} diff --git a/containermetadata/containermetadata_test.go b/containermetadata/containermetadata_test.go new file mode 100644 index 00000000..b646734a --- /dev/null +++ b/containermetadata/containermetadata_test.go @@ -0,0 +1,405 @@ +/* + * 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 containermetadata + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "testing" + + "github.com/containerd/containerd" + "github.com/docker/docker/client" + lru "github.com/elastic/go-freelru" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +func TestExtractContainerIDFromFile(t *testing.T) { + tests := []struct { + name string + cgroupname string + expContainerID string + pid libpf.PID + expEnv containerEnvironment + }{ + { + name: "dockerv1", + cgroupname: "testdata/cgroupv1docker", + expContainerID: "ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e", + expEnv: envDocker, + }, + { + name: "kubernetesv1", + cgroupname: "testdata/cgroupv1kubernetes", + expContainerID: "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + expEnv: envKubernetes, + }, + { + name: "altkubernetesv1", + cgroupname: "testdata/cgroupv1altkubernetes", + expContainerID: "af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b", + expEnv: envKubernetes, + }, + { + name: "crikubernetesv1-a", + cgroupname: "testdata/cgroupv1crikubernetes", + expContainerID: "5dab0ec4aebed0b17e8b783c11b859f43da98335fd4973a396ed7bbdab6659f3", + expEnv: envKubernetes, + }, + { + name: "crikubernetesv1-b", + cgroupname: "testdata/cgroupv1crikubernetes2", + expContainerID: "5dab0ec4aebed0b17e8b783c11b859f43da98335fd4973a396ed7bbdab6659f3", + expEnv: envKubernetes, + }, + { + name: "crikubernetesv1-c", + cgroupname: "testdata/cgroupv1crikubernetes3", + expContainerID: "2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9", + expEnv: envKubernetes, + }, + { + name: "dockerpodsv1", + cgroupname: "testdata/cgroupv1dockerpods", + expContainerID: "dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + expEnv: envDocker | envKubernetes, + }, + { + name: "dockerv2", + cgroupname: "testdata/cgroupv2docker", + expContainerID: "8ae5d36793164a2374bd9b4ceb81c6ca57a9152bdc69eafa9ce7919d22efff0d", + expEnv: envDocker, + }, + { + name: "kubernetesv2", + cgroupname: "testdata/cgroupv2kubernetes", + expContainerID: "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + expEnv: envKubernetes, + }, + { + name: "altkubernetesv2", + cgroupname: "testdata/cgroupv2altkubernetes", + expContainerID: "6fb31c47139f555a77f6dea60260eb38006755059cec4dfac8766310306dd3ee", + expEnv: envKubernetes, + }, + { + name: "dockerpodsv2", + cgroupname: "testdata/cgroupv2dockerpods", + expContainerID: "dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + expEnv: envDocker | envKubernetes, + }, + { + name: "lxc payload", + cgroupname: "testdata/lxcpayload", + expContainerID: "hermes", + expEnv: envLxc, + }, + { + name: "lxc monitor", + cgroupname: "testdata/lxcmonitor", + expContainerID: "hermes", + expEnv: envLxc, + }, + { + name: "containerd", + cgroupname: "testdata/containerdRedis", + expContainerID: "11:perf_event:/containerd-namespace/redis-server-id", + expEnv: envContainerd, + }, + { + name: "buildkit", + cgroupname: "testdata/buildkit", + expContainerID: "vy53ljgivqn5q9axwrx1mf40l", + expEnv: envDockerBuildkit, + }, + } + + containerIDCache, err := lru.NewSynced[libpf.OnDiskFileIdentifier, containerIDEntry]( + containerIDCacheSize, libpf.OnDiskFileIdentifier.Hash32) + if err != nil { + t.Fatalf("failed to provide cache: %v", err) + } + + h := &Handler{ + containerIDCache: containerIDCache, + + // Use dummy clients to trigger the regex match in the test. + dockerClient: &client.Client{}, + kubeClientSet: &kubernetes.Clientset{}, + containerdClient: &containerd.Client{}, + } + + 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) + } + }) + } +} + +func TestGetKubernetesPodMetadata(t *testing.T) { + t.Parallel() + tests := []struct { + name string + clientset kubernetes.Interface + pid libpf.PID + expContainerID string + expContainerName string + expPodName string + err error + }{ + { + name: "findMatchingPod", + clientset: fake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "testpod-abc123-sldfj293", + Namespace: "default", + Annotations: map[string]string{}, + OwnerReferences: []v1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: "testpod-abc123", + }, + }, + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "testcontainer-ab1c", + ContainerID: "docker://" + + "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + }, + }, + }, + }, &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "testpod-def456", + Namespace: "default", + Annotations: map[string]string{}, + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "testcontainer-de2f", + ContainerID: "docker://" + + "def9697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + }, + }, + }, + }), + pid: 1, + expContainerID: "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + expContainerName: "testcontainer-ab1c", + expPodName: "testpod", + }, + { + name: "matchingPodNotFound", + clientset: fake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "testpod-abc123", + Namespace: "default", + Annotations: map[string]string{}, + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "testcontainer-ab1c", + ContainerID: "docker://" + + "abc9697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + }, + }, + }, + }, &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "testpod-def456", + Namespace: "default", + Annotations: map[string]string{}, + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "testcontainer-de2f", + ContainerID: "docker://" + + "def9697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + }, + }, + }, + }), + pid: 1, + expContainerID: "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", + err: fmt.Errorf("failed to get kubernetes pod metadata, failed to " + + "find matching kubernetes pod/container metadata for containerID, " + + "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997"), + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + containerMetadataCache, err := lru.NewSynced[string, ContainerMetadata]( + containerMetadataCacheSize, hashString) + if err != nil { + t.Fatal(err) + } + + containerIDCache, err := lru.NewSynced[libpf.OnDiskFileIdentifier, containerIDEntry]( + containerIDCacheSize, libpf.OnDiskFileIdentifier.Hash32) + if err != nil { + t.Fatalf("failed to provide cache: %v", err) + } + + instance := &Handler{ + containerMetadataCache: containerMetadataCache, + kubeClientSet: test.clientset, + dockerClient: nil, + containerIDCache: containerIDCache, + } + + 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 { + // 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) + } + } + }) + } +} + +func BenchmarkGetKubernetesPodMetadata(b *testing.B) { + for i := 0; i < b.N; i++ { + clientset := fake.NewSimpleClientset() + containerMetadataCache, err := lru.NewSynced[string, ContainerMetadata]( + containerMetadataCacheSize, hashString) + if err != nil { + b.Fatal(err) + } + + containerIDCache, err := lru.NewSynced[libpf.OnDiskFileIdentifier, containerIDEntry]( + containerIDCacheSize, libpf.OnDiskFileIdentifier.Hash32) + if err != nil { + b.Fatalf("failed to provide cache: %v", err) + } + + instance := &Handler{ + containerMetadataCache: containerMetadataCache, + kubeClientSet: clientset, + dockerClient: nil, + containerIDCache: containerIDCache, + } + for j := 100; j < 700; j++ { + testPod := fmt.Sprintf("testpod-abc%d", j) + + pod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: testPod, + Namespace: "default", + Annotations: map[string]string{}, + OwnerReferences: []v1.OwnerReference{ + { + Kind: "ReplicaSet", + Name: testPod, + }, + }, + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: fmt.Sprintf("testcontainer-%d", j), + ContainerID: "docker://" + fmt.Sprintf( + "%dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe19", + j), + }, + }, + }, + } + + file, err := os.CreateTemp("", "test_containermetadata_cgroup*") + if err != nil { + b.Fatal(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) + } + + 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) + } + instance.putCache(clientsetPod) + + split := strings.Split(file.Name(), "test_containermetadata_cgroup") + pid, err := strconv.Atoi(split[1]) + if err != nil { + b.Fatal(err) + } + + _, err = instance.GetContainerMetadata(libpf.PID(pid)) + if err != nil { + b.Fatal(err) + } + } + } +} diff --git a/containermetadata/testdata/buildkit b/containermetadata/testdata/buildkit new file mode 100644 index 00000000..d82e718d --- /dev/null +++ b/containermetadata/testdata/buildkit @@ -0,0 +1,13 @@ +12:hugetlb:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +11:blkio:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +10:perf_event:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +9:devices:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +8:cpu,cpuacct:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +7:pids:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +6:cpuset:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +5:misc:/ +4:freezer:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +3:memory:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +2:net_cls,net_prio:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +1:name=systemd:/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l +0::/docker/buildkit/vy53ljgivqn5q9axwrx1mf40l diff --git a/containermetadata/testdata/cgroupv1altkubernetes b/containermetadata/testdata/cgroupv1altkubernetes new file mode 100644 index 00000000..8f327355 --- /dev/null +++ b/containermetadata/testdata/cgroupv1altkubernetes @@ -0,0 +1,11 @@ +11:perf_event:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +10:net_cls,net_prio:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +9:devices:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +8:freezer:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +7:hugetlb:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +6:pids:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +5:memory:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +4:blkio:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +3:cpuset:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +2:cpu,cpuacct:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope +1:name=systemd:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4ae91c13_1bb1_4baa_b7c3_ce86ddcf944b.slice/docker-af24eca41b7e02d7f8991153fbc255cb7e9b79e812bdbe6d8b95538b59417e2b.scope diff --git a/containermetadata/testdata/cgroupv1crikubernetes b/containermetadata/testdata/cgroupv1crikubernetes new file mode 100644 index 00000000..df01c584 --- /dev/null +++ b/containermetadata/testdata/cgroupv1crikubernetes @@ -0,0 +1 @@ +11:cpuset:/kubepods-burstable-podd8559883-9d60-478a-8cb5-cc238a63d908.slice:cri-containerd:5dab0ec4aebed0b17e8b783c11b859f43da98335fd4973a396ed7bbdab6659f3 diff --git a/containermetadata/testdata/cgroupv1crikubernetes2 b/containermetadata/testdata/cgroupv1crikubernetes2 new file mode 100644 index 00000000..29d9d2c8 --- /dev/null +++ b/containermetadata/testdata/cgroupv1crikubernetes2 @@ -0,0 +1 @@ +1:cpuset:/what/ever/kubepods-burstable-podd8559883-9d60-478a-8cb5-cc238a63d908.slice:cri-containerd:5dab0ec4aebed0b17e8b783c11b859f43da98335fd4973a396ed7bbdab6659f3 diff --git a/containermetadata/testdata/cgroupv1crikubernetes3 b/containermetadata/testdata/cgroupv1crikubernetes3 new file mode 100644 index 00000000..7d724d75 --- /dev/null +++ b/containermetadata/testdata/cgroupv1crikubernetes3 @@ -0,0 +1,13 @@ +12:hugetlb:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +11:pids:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +10:cpu,cpuacct:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +9:blkio:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +8:freezer:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +7:memory:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +6:cpuset:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +5:perf_event:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +4:net_cls,net_prio:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +3:rdma:/ +2:devices:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +1:name=systemd:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope +0::/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod515352eb_790e_4efd_a555_45d6692fa81e.slice/cri-containerd-2b78f9fa3929001b038625607be8a97af3fb9066246513e5c15343fb52dd99d9.scope diff --git a/containermetadata/testdata/cgroupv1docker b/containermetadata/testdata/cgroupv1docker new file mode 100644 index 00000000..c7c65a85 --- /dev/null +++ b/containermetadata/testdata/cgroupv1docker @@ -0,0 +1,11 @@ +11:pids:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +10:freezer:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +9:cpuset:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +8:devices:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +7:blkio:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +6:perf_event:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +5:net_cls,net_prio:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +4:memory:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +3:hugetlb:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +2:cpu,cpuacct:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e +1:name=systemd:/docker/ffdd6f676b96f53ce556815731ca2a89d23c800f37d29976155d8c68e384337e diff --git a/containermetadata/testdata/cgroupv1dockerpods b/containermetadata/testdata/cgroupv1dockerpods new file mode 100644 index 00000000..e33feb9e --- /dev/null +++ b/containermetadata/testdata/cgroupv1dockerpods @@ -0,0 +1,13 @@ +12:hugetlb:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +11:memory:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +10:perf_event:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +9:freezer:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +8:cpuset:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +7:net_cls,net_prio:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +6:cpu,cpuacct:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +5:blkio:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +4:pids:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +3:devices:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +2:rdma:/ +1:name=systemd:/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +0::/ diff --git a/containermetadata/testdata/cgroupv1kubernetes b/containermetadata/testdata/cgroupv1kubernetes new file mode 100644 index 00000000..43e83592 --- /dev/null +++ b/containermetadata/testdata/cgroupv1kubernetes @@ -0,0 +1,13 @@ +12:hugetlb:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +11:memory:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +10:perf_event:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +9:freezer:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +8:cpuset:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +7:net_cls,net_prio:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +6:cpu,cpuacct:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +5:blkio:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +4:pids:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +3:devices:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +2:rdma:/ +1:name=systemd:/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 +0::/ diff --git a/containermetadata/testdata/cgroupv2altkubernetes b/containermetadata/testdata/cgroupv2altkubernetes new file mode 100644 index 00000000..3f22cb12 --- /dev/null +++ b/containermetadata/testdata/cgroupv2altkubernetes @@ -0,0 +1 @@ +0::/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod7ee511e9_1393_4000_a524_12b0ad30a6b3.slice/docker-6fb31c47139f555a77f6dea60260eb38006755059cec4dfac8766310306dd3ee.scope diff --git a/containermetadata/testdata/cgroupv2docker b/containermetadata/testdata/cgroupv2docker new file mode 100644 index 00000000..62884bab --- /dev/null +++ b/containermetadata/testdata/cgroupv2docker @@ -0,0 +1 @@ +0::/system.slice/docker-8ae5d36793164a2374bd9b4ceb81c6ca57a9152bdc69eafa9ce7919d22efff0d.scope diff --git a/containermetadata/testdata/cgroupv2dockerpods b/containermetadata/testdata/cgroupv2dockerpods new file mode 100644 index 00000000..f501f369 --- /dev/null +++ b/containermetadata/testdata/cgroupv2dockerpods @@ -0,0 +1 @@ +0::/docker/poda9c80282-3f6b-4d5b-84d5-a137a6668011/dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 diff --git a/containermetadata/testdata/cgroupv2kubernetes b/containermetadata/testdata/cgroupv2kubernetes new file mode 100644 index 00000000..df3dce89 --- /dev/null +++ b/containermetadata/testdata/cgroupv2kubernetes @@ -0,0 +1 @@ +0::/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997 diff --git a/containermetadata/testdata/containerdRedis b/containermetadata/testdata/containerdRedis new file mode 100644 index 00000000..74b9c913 --- /dev/null +++ b/containermetadata/testdata/containerdRedis @@ -0,0 +1 @@ +11:perf_event:/containerd-namespace/redis-server-id diff --git a/containermetadata/testdata/lxcmonitor b/containermetadata/testdata/lxcmonitor new file mode 100644 index 00000000..b4bf391d --- /dev/null +++ b/containermetadata/testdata/lxcmonitor @@ -0,0 +1 @@ +0::/lxc.monitor.hermes/system.slice/postfix.service diff --git a/containermetadata/testdata/lxcpayload b/containermetadata/testdata/lxcpayload new file mode 100644 index 00000000..282f22af --- /dev/null +++ b/containermetadata/testdata/lxcpayload @@ -0,0 +1 @@ +0::/lxc.payload.hermes/system.slice/postfix.service diff --git a/debug/log/doc.go b/debug/log/doc.go new file mode 100644 index 00000000..6a33ca73 --- /dev/null +++ b/debug/log/doc.go @@ -0,0 +1,11 @@ +/* + * 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 is a drop-in wrapper around the logrus library. +It provides access to the same features, but also adds some debugging capabilities. +*/ +package log diff --git a/debug/log/log.go b/debug/log/log.go new file mode 100644 index 00000000..b157e3e4 --- /dev/null +++ b/debug/log/log.go @@ -0,0 +1,164 @@ +/* + * 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 new file mode 100644 index 00000000..4141135e --- /dev/null +++ b/debug/log/log_bench_test.go @@ -0,0 +1,70 @@ +/* + * 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 new file mode 100644 index 00000000..39acef42 --- /dev/null +++ b/debug/log/log_test.go @@ -0,0 +1,173 @@ +/* + * 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/docs/bpf-trace.drawio.svg b/docs/bpf-trace.drawio.svg new file mode 100644 index 00000000..d3eb03de --- /dev/null +++ b/docs/bpf-trace.drawio.svg @@ -0,0 +1,4 @@ + + + +BPFTraceFrames []BPFFrameComm [15]charPID uint32Hash uint64BPFFrameFileID uint64AddrOrLine uint64FrameType uint8
 
 
 
 
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/devfiler.png b/docs/devfiler.png new file mode 100644 index 00000000..e91a30e8 Binary files /dev/null and b/docs/devfiler.png differ diff --git a/docs/gopclntab.md b/docs/gopclntab.md new file mode 100644 index 00000000..f16958b2 --- /dev/null +++ b/docs/gopclntab.md @@ -0,0 +1,471 @@ +The `gopclntab` format +====================== + +All Go executables come with additional data sections that provide a plethora of +useful information meant to be used by Go's runtime. The information remains +present even for fully static and stripped executables and is thus very valuable +because it allows us to symbolize and unwind production executables. + +This data is present for all Go target platforms, but this document's scope is +narrowed to Linux ELF executables. The format changed with Go version 1.2, 1.16, +1.18 and 1.20. This document only discusses versions >= 1.16. Some mentions of +details for older versions are made when they are known. + +## Reader and writer implementations + +Multiple different readers and writers for the `.gopclntab` format exist. + +- Readers + - Go runtime: [`runtime`][runtime] + - Go standard library: [`debug/gosym`][dbg-gosym] + - Host agent's elfgopclntab.go +- Writers + - cgo's linker: [`cmd/link/internal/ld`][cgo-linker] + +[cgo-linker]: https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go +[dbg-gosym]: https://pkg.go.dev/debug/gosym +[runtime]: https://github.com/golang/go/blob/go1.20.6/src/runtime + +With the exception of the Go runtime and the linker, none of the other +implementations support parsing inline information. The linker code tends to be +the most readable implementation of the format. + +## Data location + +The majority of information lives in an ELF section called `.gopclntab`. +Depending on the build process and linker it can also be called +`.data.rel.ro.gopclntab`. Some other information (`go:func:*` and `moduledata`) +are simply mixed into regular data sections. The presence of an ELF section to +mark the location of `.gopclntab` is typical even in stripped executables, but +by no means required. The section can additionally be marked with +`runtime.pclntab`-`runtime.epclntab` `LOCAL` ELF symbols. + +Aggressively stripped executables won't have either. A portable alternative that +works regardless of whether the executable is stripped or not is to simply scan +the whole executable for the magic bytes that introduce the gopclntab header. + +## Encoding + +For the most part the `gopclntab` information uses machine-native encoding for +structs and fields. The Go runtime essentially just creates pointers with Go +structure types to read the data when it is needed, removing the need for any +decoding/unmarshalling. Any integers are thus stored in the native byte order +of the executable's architecture. Any pointer is sized according to the target +architecture's pointer size. + +The following sections use C++ structures and types to represent the data layout +because their field layout and alignment rules are well-defined. + +## Header + +The `.gopclntab` section starts with 7 bytes whose meaning has remained the same +for all Go versions >= 1.2. They are also encoded the same regardless of target +architecture. The byte order in which the magic is stored is used to infer the +endianness of the executable. The order and offsets of the remaining fields +depend on the magic. All `uintptr` fields are offsets relative to the start of +the `.gopclntab` section. + +**Go v1.20 `.gopclntab` header format** +```c++ +struct runtime_pcHeader { + uint32_t magic; // See "Version" section. + uint8_t pad[2]; // Zero. + uint8_t minLC; // Aka "quantum". Pointer alignment. + uint8_t ptrSize; // Pointer size: 4 or 8 + int64_t nfunc; // Number of entries in the function table. + unsigned nfiles; // Number of entries in the file table. + uintptr_t textStart; // Base address for text section references. + uintptr_t funcnameOffset; // Offset to function name region. + uintptr_t cuOffset; // Offset to compilation unit table region. + uintptr_t filetabOffset; // Offset to file name region. + uintptr_t pctabOffset; // Offset to PC data region. + uintptr_t pclnOffset; // Offset to function data region. +}; +``` + +The `textStart` field does not necessarily point to the first byte of the +`.text` section and might be preceded by libc functions. It points to the first +Go generated function. + +**Magic version map** + +| Magic | Go version | +|-------------:|------------| +| `0xFFFFFFF1` | 1.20 | +| `0xFFFFFFF0` | 1.18 | +| `0xFFFFFFFA` | 1.16 | +| `0xFFFFFFFB` | 1.2 | + +**References** + +- https://github.com/golang/go/blob/go1.20.6/src/runtime/symtab.go#L414 +- https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go#L217 + +## Function table + +**Internal name:** `runtime.funcdata` + +The function table starts with an `nfunc` sized array of `runtime.functab` +records that map PCs to offsets within the `runtime.funcdata` region. Each such +offset points to a `runtime._func` record. + +**Struct that maps PCs to `runtime._func` records (Go >= 1.18)** +```c++ +struct runtime_functab { + uint32_t entryoff; // First byte of function code relative to textStart. + uint32_t funcoff; // Offset relative to the function data region start. +} +``` + +**Struct that maps PCs to `runtime._func` records (Go < 1.18)** +```c++ +struct runtime_functab { + uintptr_t entry; // First byte of function code. Absolute pointer. + uintptr_t funcoff; // Offset relative to the function data region start. +}; +``` + +The `runtime._func` struct provides a large amount of information about each +top-level (non-inlined) Go function in the executable. The `pcdata` and +`funcdata` arrays can contain optional additional information. The meaning of +the values are determined by their respective array index. + +**Go v1.18+ `runtime._func` structure** +```c++ +struct runtime__func { + uint32_t entryOff; // First byte of function code relative to textStart. + int32_t nameOff; // Function name (runtime.funcnametab offset). + int32_t args; // Number of arguments. + uint32_t deferreturn; // Information about `defer` statements (?). + uint32_t pcsp; // PC<->stack delta mappings (runtime.pcdata offset) + uint32_t pcfile; // PC<->CU file index mappings (runtime.pcdata offset) + uint32_t pcln; // PC<->Line number mappings (runtime.pcdata offset) + uint32_t npcdata; // Number of dynamic PC data offsets. + uint32_t cuOffset; // Base index of the CU (runtime.cutab index) + int32_t startLine; // Line number of the first declaration character (Go v1.20+) + uint8_t funcID; // Function ID (only set for certain RT funcs, otherwise 0) + uint8_t flag; // Unknown flags. + uint8_t _[1]; // Padding. + uint8_t nfuncdata; // Number of dynamic `go:func.*` offsets. + + // Pseudo-fields (data following immediately after) + uint32_t pcdata[npcdata]; // `runtime.pcdata` offsets. + uint32_t funcdata[nfuncdata]; // `go:func.*` offsets (Go >= v1.18). + uintptr_t funcdata[nfuncdata]; // Absolute pointers (Go < v1.18). +}; +``` + +**Go v1.20 pcdata and funcdata indices** +```go +const ( + PCDATA_UnsafePoint = 0 + PCDATA_StackMapIndex = 1 + PCDATA_InlTreeIndex = 2 + PCDATA_ArgLiveIndex = 3 + + FUNCDATA_ArgsPointerMaps = 0 + FUNCDATA_LocalsPointerMaps = 1 + FUNCDATA_StackObjects = 2 + FUNCDATA_InlTree = 3 + FUNCDATA_OpenCodedDeferInfo = 4 + FUNCDATA_ArgInfo = 5 + FUNCDATA_ArgLiveInfo = 6 + FUNCDATA_WrapInfo = 7 +) +``` + +**References** + +- https://github.com/golang/go/blob/go1.16.15/src/runtime/symtab.go#L500 +- https://github.com/golang/go/blob/go1.20.6/src/runtime/symtab.go#L582 +- https://github.com/golang/go/blob/go1.16.15/src/runtime/runtime2.go#L822 +- https://github.com/golang/go/blob/go1.18.10/src/runtime/runtime2.go#L859 +- https://github.com/golang/go/blob/go1.20.6/src/runtime/runtime2.go#L882 +- https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go#L541 +- https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go#L616 +- https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go#L637 +- https://github.com/golang/go/blob/go1.20.6/src/cmd/internal/objabi/funcdata.go +- https://github.com/golang/go/blob/go1.20.6/src/runtime/symtab.go#L316-L335 + +## Function name table + +**Internal name:** `runtime.funcnametab` + +The function name table is a region within `.gopclntab` that stores the names of +all Go functions in the executable. + +**Go v1.2 - v1.15**: The table starts with an `nfunc` sized `uint32` array that +maps function indices to offsets relative to the function name table. Each such +offset points to one of the null terminated strings that follow the offset +array. + +**Go v1.16+**: The table is a concatenation of null terminated strings. +Function names are no longer referred to by index but by their byte offset in +the function name table. + +**References** + +- https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go#L288 + +## Compilation unit table + +**Internal name:** `runtime.cutab` + +The compilation unit (CU) table is single big `uint32` array. Each index within +this array is an offset relative to the start of the function name table region. +The array is addressed by adding two indices: one that marks the beginning of a +CU within the larger array and a second one that determines the nth file +associated with that CU. + +The CU table was newly added in Go v1.16. + +**References** + +- https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go#L447 + +## PC data table + +**Internal name:** `runtime.pctab` + +The PC data table is a concatenation of a large amount of PC data "programs" +that, when evaluated, produces a monotonic sequence of 1..n `(pc_offs, +i32_value)` tuples. These sequences are typically used to map program counters +to a value. The PC offsets are relative to `funcdata.entryOff`. The purpose of +`i32_value` is decided by the field that references it. For example the PC +sequence referenced by `runtime._func`'s `pcln` field produces line number +mappings. + +PC data sequences are encoded as a repetition of two variable length integers. +The first integer is signed (zig-zag encoding) and specifies the delta to add to +the previous value to compute the next one. The initial value is always -1. The +second field is an unsigned integer that specifies the PC delta to add to the +previous program counter. A value offset of `0` marks the end of a PC sequence. + +**Example `pcln` PC data sequence and decoding** +```asm +.gopclntab:A63BE DCB 0x22 ; val: 0x10 +.gopclntab:A63BF DCB 0x01 ; pc_delta: 0x04 +.gopclntab:A63C0 DCB 0x02 ; val: 0x11 +.gopclntab:A63C1 DCB 0x01 ; pc_delta: 0x08 +.gopclntab:A63C2 DCB 0x02 ; val: 0x12 +.gopclntab:A63C3 DCB 0x02 ; pc_delta: 0x10 +.gopclntab:A63C4 DCB 0x00 ; +``` + +**References** + +- https://github.com/golang/go/blob/go1.16.15/src/cmd/internal/objfile/goobj.go#L284 + +## Module data + +**Internal name:** `runtime.firstmoduledata` + +`runtime.moduledata` is essentially a bigger version of `runtime.pcHeader`. It +stores all the information that `runtime.pcHeader` has, and more. Go versions >= +1.18 address dynamic function data with offsets relative to the start of the +`go:func.*` region and this offset is not present anywhere else. + +Other than the PC header, the module info does not have a canonical, intended +way of locating it. It lives somewhere in the middle of the regular data +sections. An ELF symbol `runtime.firstmoduledata` exists, but it is marked as +`LOCAL` and thus removed during stripping. The struct's layout varies a lot +between different Go versions, though at least it is always the same for all +versions with the same magic. + +Fortunately enough, the module data always starts with a pointer to the PC +header. Scanning all data sections for this pointer will always locate the +module data. Thanks to the duplicated information between both structs (e.g. +filetab location) we can perform additional sanity checks to weed out +false-detects. The approach is inspired by what Stephen Eckels describes in his +blog article (see [Further Reading](#further-reading)). + +```c++ +struct runtime_moduledata { + runtime_pcHeader *pcHeader; + slice funcnametab; + slice cutab; + slice filetab; + slice pctab; + slice pclntable; + slice ftab; + uintptr_t findfunctab; + uintptr_t minpc; + uintptr_t maxpc; + uintptr_t text; + uintptr_t etext; + uintptr_t noptrdata; + uintptr_t enoptrdata; + uintptr_t data; + uintptr_t edata; + uintptr_t bss; + uintptr_t ebss; + uintptr_t noptrbss; + uintptr_t enoptrbss; + uintptr_t covctrs; + uintptr_t ecovctrs; + uintptr_t end; + uintptr_t gcdata; + uintptr_t gcbss; + uintptr_t types; + uintptr_t etypes; + uintptr_t rodata; + uintptr_t gofunc; // <- that's what we need! + slice textsectmap; + slice typelinks; + slice itablinks; + slice ptab; + string pluginpath; + slice pkghashes; + string modulename; + slice modulehashes; + uint8_t hasmain; + runtime_bitvector gcdatamask; + runtime_bitvector gcbssmask; + void *typemap; + bool bad; + runtime_moduledata *next; +}; +``` + +
+ Definitions for basic runtime types + +```c++ +template +struct slice { + T* ptr; + size_t len; + size_t cap; +}; + +struct string { + uint8_t *str; + int64_t len; +}; + +struct runtime_bitvector { + int32_t n; + uint8_t *bytedata; +}; +``` + +
+ +**References** + +- https://github.com/golang/go/blob/go1.18.10/src/runtime/symtab.go#L415 +- https://github.com/golang/go/blob/go1.20.6/src/runtime/symtab.go#L434 + +## Inline instance information + +Inline information is obtained by reading `runtime._func.funcdata[FUNCDATA_InlTree]` +to locate the base offset of the inline tree for the function and then using the +`runtime._func.pcdata[PCDATA_InlTreeIndex]` PC data sequence to locate the correct +index within that inline tree for any given PC in the function. This index is +then multiplied by the size of the `runtime.inlinedCall` structure and added to the +base offset to produce the address of the `inlinedCall` instance for any given PC. + +**Go v1.16 - v1.18 `runtime.inlinedCall` structure** +```c++ +struct runtime_inlinedCall { + int16_t parent; + uint8_t funcID; + uint8_t _; + int32_t file; + int32_t line; + int32_t func_; + int32_t parentPc; +}; +``` + +**Go v1.20+ `runtime.inlinedCall` structure** +```c++ +struct runtime_inlinedCall { + uint8_t funcID; + uint8_t _[3]; + int32_t nameOff; + int32_t parentPc; + int32_t startLine; +}; +``` + +**References** + +- https://github.com/golang/go/blob/go1.18.10/src/runtime/symtab.go#L1172 +- https://github.com/golang/go/blob/go1.20.6/src/runtime/symtab.go#L1208 + +## Lookup paths + +This section provides some examples on what structures need to be traversed to +locate various function information. This omits details like relative offsets +that depend on data from the header or moduledata to keep complexity managable. + +### Locating `runtime._func` for a PC + +```mermaid +flowchart LR + pchdr[runtime.pcHeader] + ftab[Binary search with PC on\nruntime.functab array] + func[runtime._func] + + pchdr--Section offset-->ftab + ftab-->func +``` +### Resolving file names for a function + +```mermaid +flowchart LR + func[runtime._func] + pcdata[runtime.pctab] + cutab[runtime.cutab] + plus[+] + filetab[runtime.funcnametab] + pc[PC] + + func--cuOffset field-->plus + pcdata--PC specific index-->plus + pc--select entry-->pcdata + plus--index-->cutab + func--pcfile field-->pcdata + cutab--"offset"-->filetab +``` + +### Resolving line numbers + +```mermaid +flowchart LR + func[runtime._func] + pcdata[runtime.pctab] + ln[Line Number] + pc[PC] + + func--pcln field-->pcdata + pc--select entry-->pcdata + pcdata-->ln +``` + +### Resolving inline information + +```mermaid +flowchart LR + func[runtime._func] + pcdata[runtime.pctab] + gofunc[go:func.*] + pc[PC] + plus[+] + timesz["* sizeof(InlinedCall)"] + inlinedcall[runtime.InlinedCall] + + func--"funcdata[InlTree]"-->plus + func--"pcdata[InlTreeIndex]"-->pcdata + pc--select entry-->pcdata + pcdata-->timesz + timesz-->plus + gofunc--base address-->plus + plus-->inlinedcall +``` + +## Further Reading + +- https://www.mandiant.com/resources/blog/golang-internals-symbol-recovery +- https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub diff --git a/docs/network-trace.drawio.svg b/docs/network-trace.drawio.svg new file mode 100644 index 00000000..d52ab85e --- /dev/null +++ b/docs/network-trace.drawio.svg @@ -0,0 +1,4 @@ + + + +TraceCountTraceHash uint128Comm stringPodName stringContainerName stringCount uint32FramesForTraceTraceHash uint128Frames []FrameFrameID FrameIDType uint8FrameMetaDataID FrameIDLineNumber u64FunctionName stringFileName string
mapped via
TraceHash
mapped via...
1
1
1
1
mapped via
Frame.ID
mapped via...
1
1
N
N
FrameIDFileID uint128AddrOrLine u64
 
 
 
 
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/trace-pipe.drawio.svg b/docs/trace-pipe.drawio.svg new file mode 100644 index 00000000..4209c7f2 --- /dev/null +++ b/docs/trace-pipe.drawio.svg @@ -0,0 +1,4 @@ + + + +
BPF unwinds stack
BPF unwinds stack
store trace
store trace
unwind_stop
sends trace
unwind_stop...
 tail call 
 tail call 
Receive trace events
Receive trace events
traceOutChan
traceOutChan
Go channel
Go channel
per_cpu_records
per_cpu_records
BPF Map
BPF Map
Receive BPF trace
Receive BPF trace
Kernel interrupts
process
Kernel interrupts...
Resume process
Resume process
Add container and
pod name for PID
Add container and...
ReportCountForTrace
ReportCountForTrace
ReportFramesForTrace
ReportFramesForTrace
Symbolize
interpreter frames
Symbolize...
Calculate UM hash
Calculate UM hash
Yes
Yes
UM hash known?
UM hash known?
counts
counts
Report & cache trace
Report & cache trace
No
No
retrieve trace
retrieve trace
FrameMetadata
FrameMetadata

UM: REPORTER

UM: REPORTER

UM: TRACE HANDLER

UM: TRACE HANDLER

UM: TRACER

UM: TRACER

KERNEL: BPF UNWINDER

KERNEL: BPF UNWINDER
event with full trace
event with full trace
trace_events
trace_events
Perf Event Buffer
Perf Event Buffer
BPF trace known
by XXH3 hash?
BPF trace known...
No
No
Yes
Yes
umTraceCache
umTraceCache
LRU[UMHash]Void
LRU[UMHash]Void
bpfTraceCache
bpfTraceCache
LRU[BPFHash]UMHash
LRU[BPFHash]UMHash
frames
frames
 frame metadata
 frame metadata
countForTraceQueue
countForTraceQueue
FIFO queue
FIFO queue
framesForTracesQueue
framesForTracesQueue
FIFO queue
FIFO queue
frameMetadataQueue
frameMetadataQueue
FIFO queue
FIFO queue
Sum counts for same
trace hash + time
Sum counts for same...
Deduplicate
Deduplicate
Convert to 
network format
Convert to...
Convert to 
network format
Convert to...
Convert to 
network format
Convert to...
AddCounts
ForTracesRequest
AddCounts...
SetFrames
ForTracesRequest
SetFrames...
AddFrame
MetadataRequest
AddFrame...

GRPC

GRPC
periodic read & clear
periodic read & clear
periodic read & clear
periodic read & clear
periodic read & clear
periodic read & clear
Text is not SVG - cannot display
\ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..9b8b4a40 --- /dev/null +++ b/go.mod @@ -0,0 +1,122 @@ +module github.com/elastic/otel-profiling-agent + +go 1.21.6 + +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 + 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/minio/sha256-simd v1.0.1 + github.com/peterbourgon/ff/v3 v3.4.0 + github.com/prometheus/procfs v0.12.0 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 + github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 + 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 +) + +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/containerd/cgroups v1.1.0 // indirect + github.com/containerd/continuity v0.4.2 // 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/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/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/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/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20230426061923-93006964c1fc // 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/locker v1.0.1 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + 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/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 + 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 + 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 + 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/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 + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..e752d45c --- /dev/null +++ b/go.sum @@ -0,0 +1,410 @@ +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= +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/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/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/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/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= +github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +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/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/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-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= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +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/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= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +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-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= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +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/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= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.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= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/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/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= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +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/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/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= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +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/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= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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/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/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= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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/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/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= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +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/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= +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/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= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-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/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/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/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/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/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= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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/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/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/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= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +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= +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= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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/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= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/host/host.go b/host/host.go new file mode 100644 index 00000000..772d0417 --- /dev/null +++ b/host/host.go @@ -0,0 +1,66 @@ +/* + * 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 host implements types and methods specific to interacting with eBPF maps. +package host + +import ( + "encoding/binary" + "fmt" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" +) + +// TraceHash is used for unique identifiers for traces, and is required to be 64-bits +// due to the constraints imposed by the eBPF maps, unlike the larger TraceHash used +// outside the host agent. +type TraceHash uint64 + +// FileID is used for unique identifiers for files, and is required to be 64-bits +// due to the constraints imposed by the eBPF maps, unlike the larger FileID used +// outside the host agent. +type FileID uint64 + +// FileIDFromBytes parses a byte slice into the internal data representation for a file ID. +func FileIDFromBytes(b []byte) (FileID, error) { + if len(b) != 8 { + return FileID(0), fmt.Errorf("invalid length for bytes '%v': %d", b, len(b)) + } + return FileID(binary.BigEndian.Uint64(b[0:8])), nil +} + +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 { + 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 +} + +type Trace struct { + Comm string + Frames []Frame + Hash TraceHash + KTime libpf.KTime + PID libpf.PID +} diff --git a/hostmetadata/agent/agent.go b/hostmetadata/agent/agent.go new file mode 100644 index 00000000..0aeb6a34 --- /dev/null +++ b/hostmetadata/agent/agent.go @@ -0,0 +1,81 @@ +/* + * 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 agent + +import ( + "fmt" + "os" + + "github.com/elastic/otel-profiling-agent/config" + "github.com/elastic/otel-profiling-agent/libpf/vc" +) + +// Agent metadata keys +const ( + // Build metadata + keyAgentVersion = "agent:version" + keyAgentRevision = "agent:revision" + keyAgentBuildTimestamp = "agent:build_timestamp" + keyAgentStartTimeMilli = "agent:start_time_milli" + + // Environment metadata + keyAgentEnvHTTPSProxy = "agent:env_https_proxy" + + // Configuration metadata + keyAgentConfigBpfLoglevel = "agent:config_bpf_log_level" + keyAgentConfigBpfLogSize = "agent:config_bpf_log_size" + keyAgentConfigCacheDirectory = "agent:config_cache_directory" + keyAgentConfigCollectionAgentAddr = "agent:config_ca_address" + keyAgentConfigurationFile = "agent:config_file" + 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" + keyAgentConfigMaxElementsPerInterval = "agent:config_max_elements_per_interval" + keyAgentConfigVerbose = "agent:config_verbose" + keyAgentConfigProbabilisticInterval = "agent:config_probabilistic_interval" + keyAgentConfigProbabilisticThreshold = "agent:config_probabilistic_threshold" + // nolint:gosec + keyAgentConfigPresentCPUCores = "agent:config_present_cpu_cores" +) + +// AddMetadata adds agent metadata to the result map. +func AddMetadata(result map[string]string) { + result[keyAgentVersion] = vc.Version() + result[keyAgentRevision] = vc.Revision() + + result[keyAgentBuildTimestamp] = vc.BuildTimestamp() + result[keyAgentStartTimeMilli] = fmt.Sprintf("%d", config.StartTime().UnixMilli()) + + bpfLogLevel, bpfLogSize := config.BpfVerifierLogSetting() + result[keyAgentConfigBpfLoglevel] = fmt.Sprintf("%d", bpfLogLevel) + result[keyAgentConfigBpfLogSize] = fmt.Sprintf("%d", 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[keyAgentConfigTags] = config.Tags() + result[keyAgentConfigTracers] = config.Tracers() + result[keyAgentConfigKnownTracesEntries] = fmt.Sprintf("%d", config.TraceCacheEntries()) + result[keyAgentConfigMapScaleFactor] = fmt.Sprintf("%d", config.MapScaleFactor()) + result[keyAgentConfigMaxElementsPerInterval] = + fmt.Sprintf("%d", config.MaxElementsPerInterval()) + result[keyAgentConfigVerbose] = fmt.Sprintf("%v", config.Verbose()) + result[keyAgentConfigProbabilisticInterval] = + config.GetTimes().ProbabilisticInterval().String() + result[keyAgentConfigProbabilisticThreshold] = + fmt.Sprintf("%d", config.ProbabilisticThreshold()) + result[keyAgentConfigPresentCPUCores] = + fmt.Sprintf("%d", config.PresentCPUCores()) + result[keyAgentEnvHTTPSProxy] = os.Getenv("HTTPS_PROXY") +} diff --git a/hostmetadata/azure/azure.go b/hostmetadata/azure/azure.go new file mode 100644 index 00000000..cfe79003 --- /dev/null +++ b/hostmetadata/azure/azure.go @@ -0,0 +1,208 @@ +/* + * 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 azure + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + "strings" + "time" + + "github.com/elastic/otel-profiling-agent/hostmetadata/instance" + log "github.com/sirupsen/logrus" +) + +const azurePrefix = "azure:" + +// Compute stores computing related metadata information of an Azure instance. +type Compute struct { + Environment string `json:"azEnvironment"` + Location string `json:"location"` + Name string `json:"name"` + VMID string `json:"vmId"` + Tags string `json:"tags"` + Zone string `json:"zone"` + VMSize string `json:"vmSize"` + Offer string `json:"offer"` + OsType string `json:"osType"` + Publisher string `json:"publisher"` + Sku string `json:"sku"` + Version string `json:"version"` + SubscriptionID string `json:"subscriptionId"` +} + +// Network stores the network related metadata information of an Azure instance. +type Network struct { + Interface []IPInterface `json:"interface"` +} + +// IPInterface stores layer 2 and 3 metadata +type IPInterface struct { + IPv4 IPInfo `json:"ipv4"` + IPv6 IPInfo `json:"ipv6"` + Mac string `json:"macAddress"` +} + +// IPInfo holds the available IP information for a particular IP family on an interface. +type IPInfo struct { + Addr []IPAddr `json:"ipAddress"` + Subnet []IPSubnet `json:"subnet"` +} + +// IPAddr holds the private and public IP address of an interface. +type IPAddr struct { + PublicIP string `json:"publicIpAddress"` + PrivateIP string `json:"privateIpAddress"` +} + +// IPSubnet stores the subnet related information to an IPAddr. +type IPSubnet struct { + Address string `json:"address"` + Prefix string `json:"prefix"` +} + +// IMDS holds the metadata information of a Azure instance. +type IMDS struct { + Compute Compute `json:"compute"` + Network Network `json:"network"` +} + +// AddMetadata adds metadata from the Azure metadata service into the provided map. +// This is safe to call even if the instance isn't running on Azure. +// Added keys are the metadata path in the metadata service, prefixed with 'azure:'. +// Synthetic metadata is also added, prefixed with 'instance:'. +// Failures (missing keys, etc) are logged and ignored. +// +// We extract the Azure metadata according to the information at +// nolint:lll +// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#endpoint-categories +// +// - 169.254.169.254 is the standard endpoint for the instance metadata service in all clouds. +// One of the few things all cloud providers agree upon +// - 169.254.0.0/16 addresses are link-local IP addresses (traffic is non-routable, and won't +// leave the local network segment). In practice, the http server that actually answers the +// requests lives inside the instance hardware +// - There is no TLS with the metadata service. Both trust and data protection are provided by +// the non-routability of the traffic (requests are handled locally, inside boundaries that are +// implicitly trusted by the user). If that http server goes down, someone working at the cloud +// provider will get paged. +func AddMetadata(result map[string]string) { + var PTransport = &http.Transport{Proxy: nil} + + client := http.Client{Transport: PTransport} + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, + "http://169.254.169.254/metadata/instance", http.NoBody) + if err != nil { + log.Errorf("Failed to create Azure metadata query: %v", err) + return + } + req.Header.Add("Metadata", "True") + + q := req.URL.Query() + q.Add("format", "json") + q.Add("api-version", "2020-09-01") + req.URL.RawQuery = q.Encode() + + resp, err := client.Do(req) + if err != nil { + log.Warnf("Azure metadata client couldn't be created, skipping metadata collection") + return + } + defer resp.Body.Close() + + var imds IMDS + if err := json.NewDecoder(resp.Body).Decode(&imds); err != nil { + log.Errorf("Failed to parse Azure metadata: %v", err) + return + } + + populateResult(result, &imds) +} + +// populateResult converts the given answer from Azure in imds into +// our internal representation in result. +func populateResult(result map[string]string, imds *IMDS) { + v := reflect.ValueOf(imds.Compute) + t := reflect.TypeOf(imds.Compute) + for i := 0; i < v.NumField(); i++ { + fieldName := t.Field(i).Name + fieldValue := v.Field(i).Interface().(string) + if fieldValue == "" { + // Don't store empty values. + continue + } + result[azurePrefix+"compute/"+strings.ToLower(fieldName)] = fieldValue + } + + // Used to temporarily hold synthetic metadata + ipAddrs := map[string][]string{ + instance.KeyPrivateIPV4s: make([]string, 0), + instance.KeyPrivateIPV6s: make([]string, 0), + instance.KeyPublicIPV4s: make([]string, 0), + instance.KeyPublicIPV6s: make([]string, 0), + } + + for i, iface := range imds.Network.Interface { + result[azurePrefix+"network/interface/"+fmt.Sprintf("%d/macaddress", i)] = iface.Mac + for j, ipv4 := range iface.IPv4.Addr { + keyPrefix := azurePrefix + "network/interface/" + + fmt.Sprintf("%d/ipv4/ipaddress/%d/", i, j) + if ipv4.PrivateIP != "" { + result[keyPrefix+"privateipaddress"] = ipv4.PrivateIP + ipAddrs[instance.KeyPrivateIPV4s] = append(ipAddrs[instance.KeyPrivateIPV4s], + ipv4.PrivateIP) + } + if ipv4.PublicIP != "" { + result[keyPrefix+"publicipaddress"] = ipv4.PublicIP + ipAddrs[instance.KeyPublicIPV4s] = append(ipAddrs[instance.KeyPublicIPV4s], + ipv4.PublicIP) + } + } + for j, netv4 := range iface.IPv4.Subnet { + keyPrefix := azurePrefix + "network/interface/" + + fmt.Sprintf("%d/ipv4/subnet/%d/", i, j) + if netv4.Address != "" { + result[keyPrefix+"address"] = netv4.Address + } + if netv4.Prefix != "" { + result[keyPrefix+"prefix"] = netv4.Prefix + } + } + for j, ipv6 := range iface.IPv6.Addr { + keyPrefix := azurePrefix + "network/interface/" + + fmt.Sprintf("%d/ipv6/ipaddress/%d/", i, j) + if ipv6.PrivateIP != "" { + result[keyPrefix+"privateipaddress"] = ipv6.PrivateIP + ipAddrs[instance.KeyPrivateIPV6s] = append(ipAddrs[instance.KeyPrivateIPV6s], + ipv6.PrivateIP) + } + if ipv6.PublicIP != "" { + result[keyPrefix+"publicipaddress"] = ipv6.PublicIP + ipAddrs[instance.KeyPublicIPV6s] = append(ipAddrs[instance.KeyPublicIPV6s], + ipv6.PublicIP) + } + } + for j, netv6 := range iface.IPv6.Subnet { + keyPrefix := azurePrefix + "network/interface/" + + fmt.Sprintf("%d/ipv6/subnet/%d/", i, j) + if netv6.Address != "" { + result[keyPrefix+"address"] = netv6.Address + } + if netv6.Prefix != "" { + result[keyPrefix+"prefix"] = netv6.Prefix + } + } + } + + instance.AddToResult(ipAddrs, result) +} diff --git a/hostmetadata/azure/azure_test.go b/hostmetadata/azure/azure_test.go new file mode 100644 index 00000000..bea29fe5 --- /dev/null +++ b/hostmetadata/azure/azure_test.go @@ -0,0 +1,168 @@ +/* + * 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 azure + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +// nolint:lll +const fakeAzureAnswer = `{ + "compute": { + "azEnvironment": "AzurePublicCloud", + "customData": "", + "isHostCompatibilityLayerVm": "false", + "licenseType": "", + "location": "westeurope", + "name": "bar-test", + "offer": "UbuntuServer", + "osProfile": { + "adminUsername": "azureuser", + "computerName": "bar-test" + }, + "osType": "Linux", + "placementGroupId": "", + "plan": { + "name": "", + "product": "", + "publisher": "" + }, + "platformFaultDomain": "0", + "platformUpdateDomain": "0", + "provider": "Microsoft.Compute", + "publicKeys": [ + { + "keyData": "ssh-rsa AAAAB3NzaC1yhMLIRQxCVYTdesFRQ+0= generated-by-azure\r\n", + "path": "/home/azureuser/.ssh/authorized_keys" + } + ], + "publisher": "Canonical", + "resourceGroupName": "cloud-shell-storage-westeurope", + "resourceId": "/subscriptions/ebdce8e8-f00-e091c79f86/resourceGroups/cloud-shell-storage-westeurope/providers/Microsoft.Compute/virtualMachines/bar-test", + "securityProfile": { + "secureBootEnabled": "false", + "virtualTpmEnabled": "false" + }, + "sku": "18.04-LTS", + "storageProfile": { + "dataDisks": [], + "imageReference": { + "id": "", + "offer": "UbuntuServer", + "publisher": "Canonical", + "sku": "18.04-LTS", + "version": "latest" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "diffDiskSettings": { + "option": "" + }, + "diskSizeGB": "30", + "encryptionSettings": { + "enabled": "false" + }, + "image": { + "uri": "" + }, + "managedDisk": { + "id": "/subscriptions/ebdce8e8-f00-e091c79f86/resourceGroups/cloud-shell-storage-westeurope/providers/Microsoft.Compute/disks/bar-test_OsDisk_1_c0ffeec7c6bd7", + "storageAccountType": "Standard_LRS" + }, + "name": "bar-test_OsDisk_1_c0ffeec7c6bd7", + "osType": "Linux", + "vhd": { + "uri": "" + }, + "writeAcceleratorEnabled": "false" + } + }, + "subscriptionId": "ebdce8e8-f00-e091c79f86", + "tags": "baz:bash;foo:bar", + "tagsList": [], + "version": "18.04.202103250", + "vmId": "1576434a-f66c-4ffe-abba-44b6a8f8", + "vmScaleSetName": "", + "vmSize": "Standard_DS1_v2", + "zone": "testzone" + }, + "network": { + "interface": [ + { + "ipv4": { + "ipAddress": [ + { + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "20.73.42.73" + }, + { + "privateIpAddress": "10.0.0.5", + "publicIpAddress": "20.73.42.74" + } + ], + "subnet": [ + { + "address": "10.0.0.0", + "prefix": "24" + } + ] + }, + "ipv6": { + "ipAddress": [] + }, + "macAddress": "0022488250E5" + } + ] + } +}` + +var expectedResult = map[string]string{ + "azure:compute/environment": "AzurePublicCloud", + "azure:compute/location": "westeurope", + "azure:compute/name": "bar-test", + "azure:compute/offer": "UbuntuServer", + "azure:compute/ostype": "Linux", + "azure:compute/publisher": "Canonical", + "azure:compute/sku": "18.04-LTS", + "azure:compute/subscriptionid": "ebdce8e8-f00-e091c79f86", + "azure:compute/version": "18.04.202103250", + "azure:compute/vmid": "1576434a-f66c-4ffe-abba-44b6a8f8", + "azure:compute/vmsize": "Standard_DS1_v2", + "azure:compute/tags": "baz:bash;foo:bar", + "azure:compute/zone": "testzone", + "azure:network/interface/0/ipv4/ipaddress/0/privateipaddress": "10.0.0.4", + "azure:network/interface/0/ipv4/ipaddress/0/publicipaddress": "20.73.42.73", + "azure:network/interface/0/ipv4/ipaddress/1/privateipaddress": "10.0.0.5", + "azure:network/interface/0/ipv4/ipaddress/1/publicipaddress": "20.73.42.74", + "azure:network/interface/0/ipv4/subnet/0/address": "10.0.0.0", + "azure:network/interface/0/ipv4/subnet/0/prefix": "24", + "azure:network/interface/0/macaddress": "0022488250E5", + "instance:public-ipv4s": "20.73.42.73,20.73.42.74", + "instance:private-ipv4s": "10.0.0.4,10.0.0.5", +} + +func TestPopulateResult(t *testing.T) { + var imds IMDS + result := make(map[string]string) + + azure := strings.NewReader(fakeAzureAnswer) + + if err := json.NewDecoder(azure).Decode(&imds); err != nil { + t.Fatalf("Failed to parse Azure metadata: %v", err) + } + + populateResult(result, &imds) + + if diff := cmp.Diff(expectedResult, result); diff != "" { + t.Fatalf("Metadata mismatch (-want +got):\n%s", diff) + } +} diff --git a/hostmetadata/collector.go b/hostmetadata/collector.go new file mode 100644 index 00000000..ff9cab85 --- /dev/null +++ b/hostmetadata/collector.go @@ -0,0 +1,88 @@ +/* + * 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 hostmetadata + +import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/config" + "github.com/elastic/otel-profiling-agent/hostmetadata/agent" + "github.com/elastic/otel-profiling-agent/hostmetadata/azure" + "github.com/elastic/otel-profiling-agent/hostmetadata/ec2" + "github.com/elastic/otel-profiling-agent/hostmetadata/gce" + "github.com/elastic/otel-profiling-agent/hostmetadata/host" + "github.com/elastic/otel-profiling-agent/reporter" +) + +// Collector implements host metadata collection and reporting +type Collector struct { + // caEndpoint is the collection agent endpoint, which is necessary to determine the source IP + // address from which traffic will be routed. This IP address is reported as host metadata. + caEndpoint string + // collectionInterval specifies the duration between host metadata collections. + collectionInterval time.Duration +} + +// NewCollector returns a new Collector for the specified collection agent endpoint. +func NewCollector(caEndpoint string) *Collector { + return &Collector{ + caEndpoint: caEndpoint, + + // 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 + // time every day. + collectionInterval: 23021 * time.Second, + } +} + +// GetHostMetadata returns a map that contains all host metadata key/value pairs. +func (c *Collector) GetHostMetadata() map[string]string { + result := make(map[string]string) + + agent.AddMetadata(result) + + if err := host.AddMetadata(c.caEndpoint, result); err != nil { + log.Errorf("Unable to get host metadata: %v", err) + } + + // Here we can gather more metadata, which may be dependent on the cloud provider, container + // technology, container orchestration stack, etc. + switch { + case config.RunsOnGCP(): + gce.AddMetadata(result) + case config.RunsOnAWS(): + ec2.AddMetadata(result) + case config.RunsOnAzure(): + azure.AddMetadata(result) + default: + } + + return result +} + +// StartMetadataCollection starts a goroutine that reports host metadata every collectionInterval. +func (c *Collector) StartMetadataCollection(ctx context.Context, + rep reporter.HostMetadataReporter) { + collectionTicker := time.NewTicker(c.collectionInterval) + go func() { + defer collectionTicker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-collectionTicker.C: + metadataMap := c.GetHostMetadata() + // metadataMap will always contain revision and build timestamp + rep.ReportHostMetadata(metadataMap) + } + } + }() +} diff --git a/hostmetadata/ec2/ec2.go b/hostmetadata/ec2/ec2.go new file mode 100644 index 00000000..7777f7b3 --- /dev/null +++ b/hostmetadata/ec2/ec2.go @@ -0,0 +1,229 @@ +/* + * 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 ec2 + +import ( + "fmt" + "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" +) + +// 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) +} + +type ec2Iface interface { + DescribeTags(*ec2.DescribeTagsInput) (*ec2.DescribeTagsOutput, error) +} + +// ec2MetadataClient can be nil if it cannot be built. +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)) +} + +func buildClient(region string) ec2Iface { + se := session.Must(session.NewSession()) + return ec2.New(se, aws.NewConfig().WithMaxRetries(0).WithRegion(region)) +} + +// 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" + } + } + return err.Error() +} + +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) + + // This is normal, as some keys do not exist + if err != nil { + log.Debugf("Unable to get metadata key: %s: %s", keyPath, awsErrorMessage(err)) + continue + } + result[ec2Prefix+keyPath] = value + } +} + +// list returns the list of keys at the given instance metadata service path. +func list(urlPath string) ([]string, error) { + value, err := ec2MetadataClient.GetMetadata(urlPath) + + if err != nil { + return nil, fmt.Errorf("unable to list %v: %s", urlPath, awsErrorMessage(err)) + } + + return instance.Enumerate(value), nil +} + +// 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) { + if ec2Client == nil { + ec2Client = buildClient(region) + if ec2Client == nil { + log.Warnf("EC2 client couldn't be created, skipping tag collection") + return + } + } + + descTagsOut, err := ec2Client.DescribeTags( + &ec2.DescribeTagsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("resource-id"), + Values: []*string{ + aws.String(instanceID), + }, + }, + }, + }) + + if err != nil { + log.Warnf("Unable to retrieve tags: %v", err) + return + } + + // EC2 tags have no character restrictions, therefore we store each key:value separately + for _, tag := range descTagsOut.Tags { + result[fmt.Sprintf("%stags/%s", ec2Prefix, *tag.Key)] = *tag.Value + } +} + +// AddMetadata adds metadata from the EC2 metadata service into the provided map. +// This is safe to call even if the instance isn't running on EC2. +// Added keys are the metadata path in the metadata service, prefixed with 'ec2:'. +// Instance tags are stored separately, prefixed with 'ec2:tags/{key}' for each tag key. +// Synthetic metadata is also added, prefixed with 'instance:'. +// Failures (missing keys, etc) are logged and ignored. +func AddMetadata(result map[string]string) { + if ec2MetadataClient == nil { + log.Warnf("EC2 metadata client couldn't be created, skipping metadata collection") + return + } + + var region string + var instanceID string + + if idDoc, err := ec2MetadataClient.GetInstanceIdentityDocument(); err == nil { + region = idDoc.Region + instanceID = idDoc.InstanceID + } else { + log.Warnf("EC2 metadata could not be collected: %v", err) + return + } + + getMetadataForKeys("", []string{ + "ami-id", + "ami-manifest-path", + "ancestor-ami-ids", + "hostname", + "instance-id", + "instance-type", + "instance-life-cycle", + "local-hostname", + "local-ipv4", + "kernel-id", + "mac", + "profile", // virtualization profile + "public-hostname", + "public-ipv4", + "product-codes", + "security-groups", + }, result) + + getMetadataForKeys("placement", []string{ + "availability-zone", + "availability-zone-id", + "region", + }, result) + + macs, err := list("network/interfaces/macs/") + if err != nil { + log.Warnf("Unable to list network interfaces: %v", err) + } + + // Used to temporarily hold synthetic metadata + ipAddrs := map[string][]string{ + instance.KeyPublicIPV4s: make([]string, 0), + instance.KeyPrivateIPV4s: make([]string, 0), + } + + for _, mac := range macs { + macPath := fmt.Sprintf("network/interfaces/macs/%s/", mac) + getMetadataForKeys(macPath, []string{ + "device-number", + "interface-id", + "local-hostname", + "local-ipv4s", + "mac", + "owner-id", + "public-hostname", + "public-ipv4s", + "security-group-ids", + "security-groups", + "subnet-id", + "subnet-ipv4-cidr-block", + "vpc-id", + "vpc-ipv4-cidr-block", + "vpc-ipv4-cidr-blocks", + }, result) + + if ips, ok := result[ec2Prefix+macPath+"public-ipv4s"]; ok { + ipAddrs[instance.KeyPublicIPV4s] = append(ipAddrs[instance.KeyPublicIPV4s], + strings.ReplaceAll(ips, "\n", ",")) + } + + if ips, ok := result[ec2Prefix+macPath+"local-ipv4s"]; ok { + ipAddrs[instance.KeyPrivateIPV4s] = append(ipAddrs[instance.KeyPrivateIPV4s], + strings.ReplaceAll(ips, "\n", ",")) + } + + assocsPath := fmt.Sprintf("%sipv4-associations/", macPath) + assocs, err := list(assocsPath) + if err != nil { + // Nothing to worry about: there might not be any associations + log.Debugf("Unable to list ipv4 associations: %v", err) + } + for _, assoc := range assocs { + getMetadataForKeys(assocsPath, []string{assoc}, result) + } + } + + instance.AddToResult(ipAddrs, result) + addTags(region, instanceID, result) +} diff --git a/hostmetadata/ec2/ec2_test.go b/hostmetadata/ec2/ec2_test.go new file mode 100644 index 00000000..d054b5ba --- /dev/null +++ b/hostmetadata/ec2/ec2_test.go @@ -0,0 +1,135 @@ +/* + * 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 ec2 + +import ( + "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" +) + +type fakeEC2Metadata struct { + metadata map[string]string +} + +type fakeEC2Tags struct { + tags []*ec2.TagDescription +} + +func (e *fakeEC2Metadata) GetMetadata(path string) (string, error) { + value, found := e.metadata[path] + if !found { + return "", fmt.Errorf("%s not found", path) + } + + return value, nil +} + +func (e *fakeEC2Metadata) GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, + error) { + return ec2metadata.EC2InstanceIdentityDocument{}, nil +} + +func (e *fakeEC2Tags) DescribeTags(_ *ec2.DescribeTagsInput, +) (*ec2.DescribeTagsOutput, error) { + return &ec2.DescribeTagsOutput{Tags: e.tags}, nil +} + +func TestAddMetadata(t *testing.T) { + ec2MetadataClient = &fakeEC2Metadata{ + metadata: map[string]string{ + "ami-id": "ami-1234", + "ami-manifest-path": "(unknown)", + "ancestor-ami-ids": "ami-2345", + "hostname": "ec2.internal", + "instance-id": "i-abcdef", + "instance-type": "m5.large", + "instance-life-cycle": "on-demand", + "local-hostname": "compute-internal", + "local-ipv4": "172.16.1.1", + "kernel-id": "aki-1419e57b", + "mac": "0e:0f:00:01:02:03", + "profile": "default-hvm", + "public-hostname": "ec2-10-eu-west-1.compute.amazonaws.com", + "public-ipv4": "1.2.3.4", + "product-codes": "foobarbaz", + "security-groups": "default\nlaunch-wizard-1", + + "placement/availability-zone": "us-east-2c", + "placement/availability-zone-id": "use2-az3", + "placement/region": "us-east-2", + + "network/interfaces/macs/": "123\n456\n789", + "network/interfaces/macs/123/device-number": "1", + "network/interfaces/macs/456/local-ipv4s": "1.2.3.4\n5.6.7.8", + "network/interfaces/macs/456/public-ipv4s": "9.9.9.9\n8.8.8.8", + "network/interfaces/macs/789/public-ipv4s": "4.3.2.1", + "network/interfaces/macs/789/ipv4-associations/": "7.7.7.7\n4.4.4.4", + "network/interfaces/macs/789/ipv4-associations/7.7.7.7": "77.77.77.77", + "network/interfaces/macs/789/ipv4-associations/4.4.4.4": "44.44.44.44", + }, + } + + ec2Client = &fakeEC2Tags{ + tags: []*ec2.TagDescription{ + { + Key: aws.String("foo"), + Value: aws.String("bar"), + }, + { + Key: aws.String("baz"), + Value: aws.String("value1-value2"), + }, + }, + } + + result := make(map[string]string) + AddMetadata(result) + + expected := map[string]string{ + "ec2:ami-id": "ami-1234", + "ec2:ami-manifest-path": "(unknown)", + "ec2:ancestor-ami-ids": "ami-2345", + "ec2:hostname": "ec2.internal", + "ec2:instance-id": "i-abcdef", + "ec2:instance-type": "m5.large", + "ec2:instance-life-cycle": "on-demand", + "ec2:local-hostname": "compute-internal", + "ec2:local-ipv4": "172.16.1.1", + "ec2:kernel-id": "aki-1419e57b", + "ec2:mac": "0e:0f:00:01:02:03", + "ec2:profile": "default-hvm", + "ec2:public-hostname": "ec2-10-eu-west-1.compute.amazonaws.com", + "ec2:public-ipv4": "1.2.3.4", + "ec2:product-codes": "foobarbaz", + "ec2:security-groups": "default\nlaunch-wizard-1", + + "ec2:placement/availability-zone": "us-east-2c", + "ec2:placement/availability-zone-id": "use2-az3", + "ec2:placement/region": "us-east-2", + + "ec2:network/interfaces/macs/123/device-number": "1", + "ec2:network/interfaces/macs/456/local-ipv4s": "1.2.3.4\n5.6.7.8", + "ec2:network/interfaces/macs/456/public-ipv4s": "9.9.9.9\n8.8.8.8", + "ec2:network/interfaces/macs/789/ipv4-associations/4.4.4.4": "44.44.44.44", + "ec2:network/interfaces/macs/789/ipv4-associations/7.7.7.7": "77.77.77.77", + "ec2:network/interfaces/macs/789/public-ipv4s": "4.3.2.1", + + "ec2:tags/foo": "bar", + "ec2:tags/baz": "value1-value2", + "instance:private-ipv4s": "1.2.3.4,5.6.7.8", + "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) + } +} diff --git a/hostmetadata/gce/client.go b/hostmetadata/gce/client.go new file mode 100644 index 00000000..fa4ac3f8 --- /dev/null +++ b/hostmetadata/gce/client.go @@ -0,0 +1,36 @@ +/* + * 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 gce + +import 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. +type gceMetadataClient struct { +} + +// gceMetadataIface is an interface for the GCE metadata client +type gceMetadataIface interface { + Get(p string) (string, error) + InstanceTags() ([]string, error) + OnGCE() bool +} + +// Get forwards to gcemetadata.Get +func (*gceMetadataClient) Get(p string) (string, error) { + return gcemetadata.Get(p) +} + +// InstanceTags forwards to gcemetadata.InstanceTags +func (*gceMetadataClient) InstanceTags() ([]string, error) { + return gcemetadata.InstanceTags() +} + +// OnGCE forwards to gcemetadata.OnGCE +func (*gceMetadataClient) OnGCE() bool { + return gcemetadata.OnGCE() +} diff --git a/hostmetadata/gce/gce.go b/hostmetadata/gce/gce.go new file mode 100644 index 00000000..df7692fa --- /dev/null +++ b/hostmetadata/gce/gce.go @@ -0,0 +1,124 @@ +/* + * 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 gce + +import ( + "fmt" + "path" + "strings" + + "github.com/elastic/otel-profiling-agent/hostmetadata/instance" + log "github.com/sirupsen/logrus" +) + +const gcePrefix = "gce:" + +var gceClient gceMetadataIface = &gceMetadataClient{} + +// list returns the list of keys at the given instance metadata service path. +func list(urlPath string) ([]string, error) { + resp, err := gceClient.Get(urlPath) + if err != nil { + return nil, fmt.Errorf("unable to list %v: %v", urlPath, err) + } + + return instance.Enumerate(resp), nil +} + +func getMetadataForKeys(prefix string, suffix []string, result map[string]string) { + for i := range suffix { + keyPath := path.Join(prefix, suffix[i]) + value, err := gceClient.Get(keyPath) + if err != nil { + // Not all keys are expected to exist + log.Debugf("Unable to get metadata key %s: %v", keyPath, err) + continue + } + result[gcePrefix+keyPath] = value + } +} + +// AddMetadata gathers a subset of GCE instance metadata, and adds it to the result map. +// This is safe to call even if the instance isn't running on GCE. +// The added keys are the metadata path in the metadata service, prefixed with 'gce:'. +// Synthetic metadata is also added, prefixed with 'instance:'. +// Failures (missing keys, etc) are logged and ignored. +func AddMetadata(result map[string]string) { + if !gceClient.OnGCE() { + return + } + + // Get metadata under instance/ + getMetadataForKeys("instance/", []string{ + "id", + "cpu-platform", + "description", + "hostname", + "image", + "machine-type", + "name", + "zone", + }, result) + + // Get the tags + tags, err := gceClient.InstanceTags() + if err != nil { + log.Warnf("Unable to retrieve tags: %v", err) + } else if len(tags) > 0 { + // GCE tags can only contain lowercase letters, numbers, and hyphens, + // therefore ';' is safe to use as a separator. + result[gcePrefix+"instance/tags"] = strings.Join(tags, ";") + } + + ifaces, err := list("instance/network-interfaces/") + if err != nil { + log.Warnf("Unable to list network interfaces: %v", err) + } + + // Used to temporarily hold synthetic metadata + ipAddrs := map[string][]string{ + instance.KeyPublicIPV4s: make([]string, 0), + instance.KeyPrivateIPV4s: make([]string, 0), + } + + // Get metadata under instance/network-interfaces/*/ + for _, iface := range ifaces { + interfacePath := fmt.Sprintf("instance/network-interfaces/%s/", iface) + + getMetadataForKeys(interfacePath, []string{ + "ip", + "gateway", + "mac", + "network", + "subnetmask", + }, result) + + if ip, ok := result[gcePrefix+interfacePath+"ip"]; ok { + ipAddrs[instance.KeyPrivateIPV4s] = append(ipAddrs[instance.KeyPrivateIPV4s], ip) + } + + accessConfigs, err := list(fmt.Sprintf("%saccess-configs/", interfacePath)) + if err != nil { + // There might not be any access configurations + log.Debugf("Unable to list access configurations: %v", err) + } + + // 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)) + + getMetadataForKeys(accessConfigPath, []string{"external-ip"}, result) + if ip, ok := result[gcePrefix+accessConfigPath+"/external-ip"]; ok { + ipAddrs[instance.KeyPublicIPV4s] = append(ipAddrs[instance.KeyPublicIPV4s], ip) + } + } + } + + instance.AddToResult(ipAddrs, result) +} diff --git a/hostmetadata/gce/gce_test.go b/hostmetadata/gce/gce_test.go new file mode 100644 index 00000000..9f11c780 --- /dev/null +++ b/hostmetadata/gce/gce_test.go @@ -0,0 +1,89 @@ +/* + * 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 gce + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +type fakeGCEMetadata struct { + tags []string + metadata map[string]string +} + +func (e *fakeGCEMetadata) Get(path string) (string, error) { + res, found := e.metadata[path] + if !found { + return "", fmt.Errorf("%s not found", path) + } + return res, nil +} + +func (e *fakeGCEMetadata) InstanceTags() ([]string, error) { + return e.tags, nil +} + +func (e *fakeGCEMetadata) OnGCE() bool { + return true +} + +func TestAddMetadata(t *testing.T) { + gceClient = &fakeGCEMetadata{ + tags: []string{"foo", "bar", "baz"}, + metadata: map[string]string{ + "instance/id": "1234", + "instance/cpu-platform": "Intel Cascade Lake", + "instance/machine-type": "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/network-interfaces/": "0\n1\n2", + "instance/network-interfaces/0/ip": "1.1.1.1", + "instance/network-interfaces/0/network": "networks/default", + "instance/network-interfaces/0/subnetmask": "255.255.240.0", + "instance/network-interfaces/1/gateway": "22.22.22.22", + "instance/network-interfaces/2/mac": "3:3:3", + "instance/network-interfaces/2/access-configs/": "0\n1\n2", + "instance/network-interfaces/2/access-configs/0/external-ip": "7.7.7.7", + "instance/network-interfaces/2/access-configs/1/external-ip": "8.8.8.8", + "instance/network-interfaces/2/access-configs/2/external-ip": "9.9.9.9", + "instance/image": "gke-node-images/global", + }, + } + result := make(map[string]string) + 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", + "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/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) + } +} diff --git a/hostmetadata/host/cpuid.go b/hostmetadata/host/cpuid.go new file mode 100644 index 00000000..410458e2 --- /dev/null +++ b/hostmetadata/host/cpuid.go @@ -0,0 +1,133 @@ +//go:build linux +// +build 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 host + +import ( + "fmt" + "os" + "runtime" + "strconv" + "strings" + "sync" + + "github.com/klauspost/cpuid/v2" + "golang.org/x/sync/errgroup" + "golang.org/x/sys/unix" + + log "github.com/sirupsen/logrus" +) + +const CPUOnlinePath = "/sys/devices/system/cpu/online" +const CPUPresentPath = "/sys/devices/system/cpu/present" + +// This variable holds the CPUInfo: as key the core ID, as value the CPUID fetched from it. +// We duplicate the data at startup to be able to split and aggregate +// with more dimensions later. +var _CPUIDs map[int]*cpuid.CPUInfo + +// CPUID is an expensive instruction so we want to call it only once +// when we load the host-agent. +// We rely on online CPUs as count of logical cores where we will run CPUID. +// If reading online CPUs fails, we will leave the list of CPUInfo empty, +// and no CPU metadata will be collected. +func init() { + coreIDs, err := ParseCPUCoreIDs(CPUOnlinePath) + if err != nil { + // We could panic here, but we prefer not to fail and have missing metadata. + log.Errorf("Could not get number of CPUs: %v", err) + return + } + _CPUIDs, err = runCPUIDOnAllCores(coreIDs) + if err != nil { + log.Warnf("Metadata might be incomplete, could not execute CPUID on all cores: %v", err) + return + } +} + +// PresentCPUCores returns the number of present CPU cores. +func PresentCPUCores() (uint16, error) { + coreIDs, err := ParseCPUCoreIDs(CPUPresentPath) + if err != nil { + return 0, fmt.Errorf("reading '%s' failed: %v", CPUPresentPath, err) + } + return uint16(len(coreIDs)), nil +} + +// This function should only be called in the init() of this package! +// It is expensive to call CPUID so we store its result into a package-scoped variable. +// This function ensures that we run CPUID on all available sockets. +func runCPUIDOnAllCores(numCPUs []int) (map[int]*cpuid.CPUInfo, error) { + ret := make(map[int]*cpuid.CPUInfo, len(numCPUs)) + + // A Mutex is required to protect the concurrent access to the CPU singleton. + mx := sync.Mutex{} + // We use an errgroup to spawn independent goroutines, one for each core. + g := errgroup.Group{} + for _, id := range numCPUs { + cpuID := id + // Each goroutine will be locked to a thread, and the thread will be scheduled + // via affinity on the logical core using its ID. + g.Go(func() error { + runtime.LockOSThread() + mask := &unix.CPUSet{} + mask.Zero() + mask.Set(cpuID) + if err := unix.SchedSetaffinity(0, mask); err != nil { + return fmt.Errorf("could not set CPU affinity on core %d: %v", cpuID, err) + } + mx.Lock() + cpuid.Detect() + ret[cpuID] = &cpuid.CPU + mx.Unlock() + return nil + }) + } + if err := g.Wait(); err != nil { + return ret, err + } + + return ret, nil +} + +// Read CPUs from /sys/device and report the core IDs as a list of integers. +func ParseCPUCoreIDs(cpuPath string) ([]int, error) { + buf, err := os.ReadFile(cpuPath) + if err != nil { + return nil, fmt.Errorf("could not read %s: %v", cpuPath, err) + } + return readCPURange(string(buf)) +} + +// Since the format of online CPUs can contain comma-separated, ranges or a single value +// we need to try and parse it in all its different forms. +// Reference: https://www.kernel.org/doc/Documentation/admin-guide/cputopology.rst +func readCPURange(cpuRangeStr string) ([]int, error) { + var cpus []int + cpuRangeStr = strings.Trim(cpuRangeStr, "\n ") + for _, cpuRange := range strings.Split(cpuRangeStr, ",") { + rangeOp := strings.SplitN(cpuRange, "-", 2) + first, err := strconv.ParseUint(rangeOp[0], 10, 32) + if err != nil { + return nil, err + } + if len(rangeOp) == 1 { + cpus = append(cpus, int(first)) + continue + } + last, err := strconv.ParseUint(rangeOp[1], 10, 32) + if err != nil { + return nil, err + } + for n := first; n <= last; n++ { + cpus = append(cpus, int(n)) + } + } + return cpus, nil +} diff --git a/hostmetadata/host/cpuid_test.go b/hostmetadata/host/cpuid_test.go new file mode 100644 index 00000000..bbc247ff --- /dev/null +++ b/hostmetadata/host/cpuid_test.go @@ -0,0 +1,51 @@ +/* + * 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 host + +import ( + "os" + "runtime" + "testing" + + "github.com/klauspost/cpuid/v2" + "github.com/stretchr/testify/assert" +) + +func TestCPUID_DetectAllCPUIDs(t *testing.T) { + coreIDs, err := ParseCPUCoreIDs(CPUOnlinePath) + if err != nil { + t.Fatalf("expected to read available CPUs, got error: %v", err) + } + + detected, err := runCPUIDOnAllCores(coreIDs) + + assert.Nil(t, err) + assert.Equal(t, runtime.NumCPU(), len(detected)) + 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) +} + +func TestCPUID_ParseOnlineCPUCoreIDs(t *testing.T) { + const onlineValuesSample = `0,3-6,8-11` + + f := prepareFakeCPUOnlineFile(t, onlineValuesSample) + defer os.Remove(f.Name()) + + coreIDs, err := ParseCPUCoreIDs(f.Name()) + assert.Nil(t, err) + assert.Equal(t, 9, len(coreIDs)) +} + +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) + } + _ = os.WriteFile(f.Name(), []byte(content), os.ModePerm) + return f +} diff --git a/hostmetadata/host/cpuinfo.go b/hostmetadata/host/cpuinfo.go new file mode 100644 index 00000000..f0f1a352 --- /dev/null +++ b/hostmetadata/host/cpuinfo.go @@ -0,0 +1,273 @@ +/* + * 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 host + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/prometheus/procfs" + "github.com/prometheus/procfs/sysfs" + + log "github.com/sirupsen/logrus" +) + +const ( + // Keys we get from procfs + keyCPUVendorID = "vendor" + keyCPUModel = "model" + keyCPUModelName = "model-name" + keyCPUStepping = "stepping" + keyCPUFlags = "flags" + keyCPUBugs = "bugs" + keyCPUMaxMhz = "clock/max-mhz" + keyCPUMinMhz = "clock/min-mhz" + keyCPUScalingCurFreqMhz = "clock/scaling-cur-freq-mhz" + keyCPUScalingDriver = "clock/scaling-driver" + keyCPUScalingGovernor = "clock/scaling-governor" + + // Keys from CPUID + keyCPUThreadsPerCore = "threads-per-core" + keyCPUCoresPerSocket = "cores-per-socket" + keyCPUNumCPUs = "cpus" + keyCPUCacheL1d = "cache/L1d-kbytes" + keyCPUCacheL1i = "cache/L1i-kbytes" + keyCPUCacheL2 = "cache/L2-kbytes" + keyCPUCacheL3 = "cache/L3-kbytes" + + // Parsed from Kernel file + keyCPUOnline = "online" + + keyPrefixCPU = "host:cpu" + + // measures + kiloMemDiv = 1024 + megaHzDiv = 1000 +) + +// We use CPUInfo from prometheus/procfs to fetch all data about all CPUs and group them as we want +// and get the caches (missing in procfs) from klauspost/cpuid, locking goroutines to CPUs + +// A map of "host:cpu:" keys to a map of socketIDs to values +type cpuInfo map[string]map[int]string + +func key(suffix string) string { + return fmt.Sprintf("%s/%s", keyPrefixCPU, suffix) +} + +func (ci cpuInfo) add(socketID int, suffix, value string) { + if value == "" { + return + } + key := key(suffix) + if _, ok := ci[key]; !ok { + ci[key] = map[int]string{} + } + ci[key][socketID] = value +} + +func (ci cpuInfo) addMany(socketID int, values map[string]string) { + for suffix, value := range values { + ci.add(socketID, suffix, value) + } +} + +// readCPUInfo will return a map with data about CPUs by reading from 3 sources: +// /proc/cpuinfo, /sys/device/system/cpu and the CPUID instruction. +func readCPUInfo() (cpuInfo, error) { + info := cpuInfo{} + sysDeviceCPUs, sysDeviceCPUFreqs, err := fetchCPUSysFs() + if err != nil { + return nil, err + } + + cpuProcInfos, err := fetchCPUProcInfo() + if err != nil { + return nil, fmt.Errorf("error reading /proc/cpuinfo: %v", err) + } + + // Online CPUs list will be used during the per-coreID iteration + // to match the coreIDs that are online + onlineCPUs, err := ParseCPUCoreIDs(CPUOnlinePath) + if err != nil { + return nil, fmt.Errorf("error reading online CPUs: %v", err) + } + + // Iterate over all the logical cores and get their topology, + // in order to group them per physical socket. + // We expect all the 3 slices fetched via sysfs and procfs to have the same + // number of entries, as they are fetched from the kernel, so we iterate + // and fetch items from them using the slice index. + for deviceID, cpu := range sysDeviceCPUs { + // We need the topology to map logical cores onto physical sockets. + topology, err := cpu.Topology() + if err != nil { + continue + } + + socketID, err := strconv.Atoi(topology.PhysicalPackageID) + // An error here should never happen but we want to log it in case it happens + if err != nil { + log.Errorf("Unable to convert socketID %s to integer: %v", + topology.PhysicalPackageID, err) + continue + } + + // Checks if the deviceID is available in the online CPUs, using siblings + siblings := topology.CoreSiblingsList + info.add(socketID, keyCPUOnline, onlineCPUsFor(siblings, onlineCPUs)) + + addCPU(info, &cpuProcInfos[deviceID], socketID) + addCPUFrequencies(info, &sysDeviceCPUFreqs[deviceID], socketID) + + // CPUID data are stored in a map with key the logical coreID, + // so we will use that instead of the index of the previous slices. + coreID, err := strconv.Atoi(topology.CoreID) + if err != nil { + log.Errorf("Unable to convert coreID %s to integer: %v", topology.CoreID, err) + continue + } + addCPUID(info, socketID, coreID) + } + + return info, nil +} + +func addCPUFrequencies(info cpuInfo, freqs *sysfs.SystemCPUCpufreqStats, socketID int) { + // We want MegaHertz and the value is originally in KiloHertz + if freqs.CpuinfoMaximumFrequency != nil { + maxVal := *freqs.CpuinfoMaximumFrequency + info.add(socketID, keyCPUMaxMhz, strconv.Itoa(int(maxVal)/megaHzDiv)) + } + if freqs.CpuinfoMinimumFrequency != nil { + minVal := *freqs.CpuinfoMinimumFrequency + info.add(socketID, keyCPUMinMhz, strconv.Itoa(int(minVal)/megaHzDiv)) + } + if freqs.ScalingCurrentFrequency != nil { + scaling := *freqs.ScalingCurrentFrequency + info.add(socketID, keyCPUScalingCurFreqMhz, strconv.Itoa(int(scaling)/megaHzDiv)) + } + info.addMany(socketID, map[string]string{ + keyCPUScalingGovernor: freqs.Governor, + keyCPUScalingDriver: freqs.Driver, + }) +} + +func addCPU(info cpuInfo, cpuProcInfos *procfs.CPUInfo, socketID int) { + // We want a comma-separated, sorted list of flags and bugs, so we sort them here + sort.Strings(cpuProcInfos.Flags) + sort.Strings(cpuProcInfos.Bugs) + info.addMany(socketID, map[string]string{ + keyCPUVendorID: cpuProcInfos.VendorID, + keyCPUModel: cpuProcInfos.Model, + keyCPUModelName: cpuProcInfos.ModelName, + keyCPUStepping: cpuProcInfos.Stepping, + keyCPUFlags: strings.Join(cpuProcInfos.Flags, ","), + keyCPUBugs: strings.Join(cpuProcInfos.Bugs, ","), + }) +} + +func addCPUID(info cpuInfo, socketID, cpuID int) { + cpuData, ok := _CPUIDs[cpuID] + if ok { + info.addMany(socketID, map[string]string{ + keyCPUThreadsPerCore: strconv.Itoa(cpuData.ThreadsPerCore), + keyCPUCoresPerSocket: strconv.Itoa(cpuData.PhysicalCores), + // We want KiloBytes and the value is originally in bytes + keyCPUCacheL1i: strconv.Itoa(cpuData.Cache.L1I / kiloMemDiv), + keyCPUCacheL1d: strconv.Itoa(cpuData.Cache.L1D / kiloMemDiv), + keyCPUCacheL2: strconv.Itoa(cpuData.Cache.L2 / kiloMemDiv), + keyCPUCacheL3: strconv.Itoa(cpuData.Cache.L3 / kiloMemDiv), + }) + + if cpuData.LogicalCores == 0 { + // cpuData.LogicalCores returns the number of physical cores times the + // number of threads that can run on each core. Architectures like KVM does + // not have physical cores. Therefore we assume the number of threads per + // core are on a single CPU. + info.add(socketID, keyCPUNumCPUs, strconv.Itoa(cpuData.ThreadsPerCore)) + } else { + info.add(socketID, keyCPUNumCPUs, strconv.Itoa(cpuData.LogicalCores)) + } + } else { + // If the map lookup changed, we populate the entries with an error string. + errorString := "ERR" + info.addMany(socketID, map[string]string{ + keyCPUThreadsPerCore: errorString, + keyCPUCoresPerSocket: errorString, + keyCPUNumCPUs: errorString, + keyCPUCacheL1i: errorString, + keyCPUCacheL1d: errorString, + keyCPUCacheL2: errorString, + keyCPUCacheL3: errorString, + }) + } +} + +func fetchCPUProcInfo() ([]procfs.CPUInfo, error) { + fs, err := procfs.NewDefaultFS() + if err != nil { + return nil, fmt.Errorf("failed to read /proc: %v", err) + } + + return fs.CPUInfo() +} + +func fetchCPUSysFs() ([]sysfs.CPU, []sysfs.SystemCPUCpufreqStats, error) { + sys, err := sysfs.NewDefaultFS() + if err != nil { + return nil, nil, fmt.Errorf("failed to read /sys filesystem: %v", err) + } + cpus, err := sys.CPUs() + if err != nil { + return nil, nil, fmt.Errorf("failed to read CPUS from /sys/device/system/cpu: %v", err) + } + freqs, err := sys.SystemCpufreq() + if err != nil { + return nil, nil, + fmt.Errorf("failed to read frequencies from /sys/device/system/cpu: %v", err) + } + + return cpus, freqs, nil +} + +func onlineCPUsFor(siblingsList string, onlineCoreIDs []int) string { + siblings, err := readCPURange(siblingsList) + if err != nil { + log.Errorf("Could not parse CPU siblings: %v", err) + } + sort.Ints(siblings) + var onlines []int + for _, c := range onlineCoreIDs { + if x := sort.SearchInts(siblings, c); x < len(siblings) && + siblings[x] == c { + onlines = append(onlines, c) + } + } + return writeCPURange(onlines) +} + +func writeCPURange(listOf []int) string { + sort.Ints(listOf) + var ret string + for i := range listOf { + if ret == "" { + ret = strconv.Itoa(listOf[i]) + continue + } + if listOf[i] == listOf[i-1]+1 { + ret = strings.TrimSuffix(ret, "-"+strconv.Itoa(listOf[i-1])) + ret += "-" + strconv.Itoa(listOf[i]) + } else { + ret += "," + strconv.Itoa(listOf[i]) + } + } + + return ret +} diff --git a/hostmetadata/host/cpuinfo_test.go b/hostmetadata/host/cpuinfo_test.go new file mode 100644 index 00000000..886c7bef --- /dev/null +++ b/hostmetadata/host/cpuinfo_test.go @@ -0,0 +1,107 @@ +/* + * 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 host + +import ( + "sort" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadCPUInfo(t *testing.T) { + info, err := readCPUInfo() + assert.Nil(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, + sort.StringsAreSorted(strings.Split(info[key(keyCPUFlags)][0], ","))) + }, + "ThreadsPerCore": func(t *testing.T) { + assert.Contains(t, info[key(keyCPUThreadsPerCore)], 0) + assert.NotEmpty(t, info[key(keyCPUThreadsPerCore)][0]) + }, + "Caches": func(t *testing.T) { + assert.Contains(t, info[key(keyCPUCacheL1i)], 0) + assert.Contains(t, info[key(keyCPUCacheL1d)], 0) + assert.Contains(t, info[key(keyCPUCacheL2)], 0) + assert.Contains(t, info[key(keyCPUCacheL3)], 0) + assert.NotEmpty(t, info[key(keyCPUCacheL1i)][0]) + assert.NotEmpty(t, info[key(keyCPUCacheL1d)][0]) + assert.NotEmpty(t, info[key(keyCPUCacheL2)][0]) + assert.NotEmpty(t, info[key(keyCPUCacheL3)][0]) + }, + "CachesIsANumber": func(t *testing.T) { + assert.Contains(t, info[key(keyCPUCacheL1i)], 0) + _, err := strconv.Atoi(info[key(keyCPUCacheL1i)][0]) + assert.Nil(t, err) + assert.Contains(t, info[key(keyCPUCacheL3)], 0) + _, err = strconv.Atoi(info[key(keyCPUCacheL3)][0]) + assert.Nil(t, err) + }, + "NumCPUs": func(t *testing.T) { + assert.Contains(t, info[key(keyCPUNumCPUs)], 0) + assert.NotEmpty(t, info[key(keyCPUNumCPUs)][0]) + }, + "CoresPerSocket": func(t *testing.T) { + assert.Contains(t, info[key(keyCPUCoresPerSocket)], 0) + 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) + }, + "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) + assert.NotEmpty(t, t, ints) + }, + } + for assertion, run := range assertions { + t.Run(assertion, run) + } +} + +func TestOnlineCPUsFor(t *testing.T) { + const siblings = `0-7` + + type args struct { + coreIDs []int + expected string + } + tests := map[string]args{ + "One_CPU_Only": {[]int{3}, `3`}, + "A_Comma": {[]int{3, 5}, `3,5`}, + "A_Range": {[]int{0, 1, 2, 3}, `0-3`}, + "A_Range_And_Single": {[]int{0, 1, 2, 5}, `0-2,5`}, + "Two_Ranges": {[]int{0, 1, 2, 5, 6, 7}, `0-2,5-7`}, + "Ranges_And_Commas": {[]int{1, 2, 4, 6, 7}, `1-2,4,6-7`}, + "Multiple_Comma": {[]int{1, 2, 4, 7}, `1-2,4,7`}, + "Multiple_Mixes_MultipleTimes": {[]int{0, 1, 3, 4, 6, 7}, `0-1,3-4,6-7`}, + } + + for name, test := range tests { + c := test + t.Run(name, func(t *testing.T) { + assert.Equal(t, c.expected, onlineCPUsFor(siblings, c.coreIDs)) + }) + } +} diff --git a/hostmetadata/host/host.go b/hostmetadata/host/host.go new file mode 100644 index 00000000..c8905a10 --- /dev/null +++ b/hostmetadata/host/host.go @@ -0,0 +1,411 @@ +/* + * 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 host + +import ( + "bytes" + "fmt" + "io" + "net" + "os" + "regexp" + "runtime" + "sort" + "strconv" + "strings" + "sync" + + "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" + + // Prefix for all the sysctl keys + keyPrefixSysctl = "host:sysctl/" + + keyTags = "host:tags" +) + +// Various sysctls we are interested in. +// net.* sysctls must be read from the root network namespace. +var sysctls = []string{ + "net.core.bpf_jit_enable", + "kernel.bpf_stats_enabled", + "kernel.unprivileged_bpf_disabled", +} + +var ValidTagRegex = regexp.MustCompile(`^[a-zA-Z0-9-:._]+$`) + +// AddMetadata adds host metadata to the result map, that is common across all environments. +// The IP address and hostname (part of the returned metadata) are evaluated in the context of +// PID 1's namespaces, in order to make the information agnostic to any container solutions. +// This may not be the best thing to do in some scenarios, but still seems to be the most sensible +// default. +func AddMetadata(caEndpoint string, result map[string]string) error { + // Extract the host part of the endpoint + // Remove the port from the endpoint in case it is present + host, _, err := net.SplitHostPort(caEndpoint) + if err != nil { + host = caEndpoint + } + + validatedTags := config.ValidatedTags() + if validatedTags != "" { + result[keyTags] = validatedTags + } + + // Get /proc/version. This is better than the information returned by `uname`, as it contains + // the version of the compiler that compiled the kernel. + kernelProcVersion, err := os.ReadFile("/proc/version") + if err != nil { + return fmt.Errorf("unable to read /proc/version: %v", err) + } + result[KeyKernelProcVersion] = sanitizeString(kernelProcVersion) + + info, err := readCPUInfo() + if err != nil { + return fmt.Errorf("unable to read CPU information: %v", err) + } + + dedupCPUInfo(result, info) + + // The rest of the metadata needs CAP_SYS_ADMIN to be collected, so we check that first + hasCapSysAdmin, err := hasCapSysAdmin() + if err != nil { + return err + } + + // We need to call the `setns` syscall to extract information (network route, hostname) from + // different namespaces. + // However, `setns` doesn't know about goroutines, it operates on OS threads. + // Therefore, the below code needs to take extra steps to make sure no other code (outside of + // this function) will execute in a different namespace. + // + // To do this, we use `runtime.LockOSThread()`, which we call from a separate goroutine. + // runtime.LockOSThread() ensures that the thread executing the goroutine will be terminated + // when the goroutine exits, which makes it impossible for the entered namespaces to be used in + // a different context than the below code. + // + // It would be doable without a goroutine, by saving and restoring the namespaces before calling + // runtime.UnlockOSThread(), but error handling makes things complicated and unsafe/dangerous. + // The below implementation is always safe to run even in the presence of errors. + // + // The only downside is that calling this function comes at the cost of sacrificing an OS + // thread, which will likely force the Go runtime to launch a new thread later. This should be + // acceptable if it doesn't happen too often. + var wg sync.WaitGroup + wg.Add(1) + + // Error result of the below goroutine. May contain multiple combined errors. + var errResult error + + go func() { + defer wg.Done() + + if hasCapSysAdmin { + // Before entering a different namespace, lock the current goroutine to a thread. + // Note that we do *not* call runtime.UnlockOSThread(): this ensures the current thread + // will exit after the goroutine finishes, which makes it impossible for other + // goroutines to enter a different namespace. + runtime.LockOSThread() + + // Try to enter root namespaces. If that fails, continue anyway as we might be able to + // gather some metadata. + utsFD, netFD := tryEnterRootNamespaces() + + // Any errors were already logged by the above function. + if utsFD != -1 { + defer unix.Close(utsFD) + } + if netFD != -1 { + defer unix.Close(netFD) + } + } else { + log.Warnf("No CAP_SYS_ADMIN capability, collecting metadata from " + + "current process namespaces") + } + + // Add sysctls to the result map + for _, sysctl := range sysctls { + sysctlValue, err2 := getSysctl(sysctl) + if err2 != nil { + errResult = multierr.Combine(errResult, err2) + continue + } + sysctlKey := keyPrefixSysctl + sysctl + result[sysctlKey] = sanitizeString(sysctlValue) + } + + // Get IP address + var ip net.IP + ip, err = getSourceIPAddress(host) + if err != nil { + errResult = multierr.Combine(errResult, err) + } else { + result[KeyIPAddress] = ip.String() + } + + // 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)) + } else { + result[KeyKernelVersion] = sanitizeString(uname.Release[:]) + result[KeyHostname] = sanitizeString(uname.Nodename[:]) + result[KeyMachine] = sanitizeString(uname.Machine[:]) + } + }() + + wg.Wait() + + if errResult != nil { + return errResult + } + + return nil +} + +const keySuffixCPUSocketID = "socketIDs" + +func keySocketID(prefix string) string { + return fmt.Sprintf("%s/%s", prefix, keySuffixCPUSocketID) +} + +// dedupCPUInfo transforms cpuInfo values into a more compact form and populates result map. +// The resulting keys and values generated from a cpuInfo key K with socket IDs 0,1,2,3 +// and values V, V, V1, V2 will be of the form: +// +// "K": "V;V1;V2" +// "K/socketIDs": "0,1;2;3" +// +// The character ';' is used as a separator for distinct values since is it highly unlikely +// that it will occur as part of the values themselves, most of which are numeric. +// TODO: Investigate alternative encoding schemes such as JSON. +func dedupCPUInfo(result map[string]string, info cpuInfo) { + for key, socketValues := range info { + // A map from CPU info values to their associated socket ids + uniques := map[string]libpf.Set[string]{} + + // Gather all unique values and their socket ids for this key + for socketID, socketValue := range socketValues { + sid := strconv.Itoa(socketID) + if _, ok := uniques[socketValue]; !ok { + uniques[socketValue] = libpf.Set[string]{} + } + uniques[socketValue][sid] = libpf.Void{} + } + values := libpf.MapKeysToSlice(uniques) + result[key] = strings.Join(values, ";") + + // Gather all socketIDs, combine them and write them to result + socketIDs := make([]string, 0, len(values)) + for _, v := range values { + sids := uniques[v].ToSlice() + sort.Slice(sids, func(a, b int) bool { + intA, _ := strconv.Atoi(sids[a]) + intB, _ := strconv.Atoi(sids[b]) + return intA < intB + }) + socketIDs = append(socketIDs, strings.Join(sids, ",")) + } + result[keySocketID(key)] = strings.Join(socketIDs, ";") + } +} + +func sanitizeString(str []byte) string { + // Trim byte array from 0x00 bytes + return string(bytes.Trim(str, "\x00")) +} + +func addressFamily(ip net.IP) (uint8, error) { + if ip.To4() != nil { + return unix.AF_INET, nil + } + if len(ip) == net.IPv6len { + return unix.AF_INET6, nil + } + return 0, fmt.Errorf("invalid IP address: %v", ip) +} + +// getSourceIPAddress returns the source IP address for the traffic destined to the specified +// domain. +func getSourceIPAddress(domain string) (net.IP, error) { + conn, err := rtnetlink.Dial(nil) + if err != nil { + return nil, fmt.Errorf("unable to open netlink connection") + } + defer conn.Close() + + dstIPs, err := net.LookupIP(domain) + if err != nil { + return nil, fmt.Errorf("unable to resolve %s: %v", domain, err) + } + if len(dstIPs) == 0 { + return nil, fmt.Errorf("unable to resolve %s: no IP address", domain) + } + + var srcIP net.IP + var lastError error + found := false + + // We might get multiple IP addresses, check all of them as some may not be routable (like an + // IPv6 address on an IPv4 network). + for _, ip := range dstIPs { + addressFamily, err := addressFamily(ip) + if err != nil { + return nil, fmt.Errorf("unable to get address family for %s: %v", ip.String(), err) + } + + req := &rtnetlink.RouteMessage{ + Family: addressFamily, + Table: unix.RT_TABLE_MAIN, + Attributes: rtnetlink.RouteAttributes{ + Dst: ip, + }, + } + + routes, err := conn.Route.Get(req) + if err != nil { + lastError = fmt.Errorf("unable to get route to %s (%s): %v", domain, ip.String(), err) + continue + } + + if len(routes) == 0 { + continue + } + if len(routes) > 1 { + // More than 1 route! + // This doesn't look like this should ever happen (even in the presence of overlapping + // routes with same metric, this will return a single route). + // May be a leaky abstraction/artifact from the way the netlink API works? + // Regardless, this seems ok to ignore, but log just in case. + log.Warnf("Found multiple (%d) routes to %v; first 2 routes: %#v and %#v", + len(routes), domain, routes[0], routes[1]) + } + + // Sanity-check the result, in case the source address is left uninitialized + if len(routes[0].Attributes.Src) == 0 { + lastError = fmt.Errorf( + "unable to get route to %s (%s): no source IP address", domain, ip.String()) + continue + } + + srcIP = routes[0].Attributes.Src + found = true + break + } + + if !found { + return nil, fmt.Errorf("no route found to %s: %v", domain, lastError) + } + + log.Debugf("Traffic to %v is routed from %v", domain, srcIP.String()) + return srcIP, nil +} + +func hasCapSysAdmin() (bool, error) { + caps, err := capability.NewPid2(0) // 0 == current process + if err != nil { + return false, fmt.Errorf("unable to get process capabilities") + } + err = caps.Load() + if err != nil { + return false, fmt.Errorf("unable to load process capabilities") + } + return caps.Get(capability.EFFECTIVE, capability.CAP_SYS_ADMIN), nil +} + +// getSysctl returns the value of a particular sysctl (eg: "net.core.bpf_jit_enable"). +func getSysctl(sysctl string) ([]byte, error) { + // "net.core.bpf_jit_enable" => /proc/sys/net/core/bpf_jit_enable + path := "/proc/sys/" + strings.ReplaceAll(sysctl, ".", "/") + + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("unable to open %v: %v", path, err) + } + defer file.Close() + + contents, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("unable to read %v: %v", path, err) + } + + if len(contents) == 0 { + return []byte{}, nil + } + + // Remove the trailing newline if present + length := len(contents) + if contents[length-1] == 0x0a { + contents = contents[:length-1] + } + + return contents, nil +} + +// ValidateTags parses and validates user-specified tags. +// Each tag must match ValidTagRegex with ';' used as a separator. +// Tags that can't be validated are dropped. +// The empty string is returned if no tags can be validated. +func ValidateTags(tags string) string { + if tags == "" { + return "" + } + + splitTags := strings.Split(tags, ";") + validatedTags := make([]string, 0, len(splitTags)) + + for _, tag := range splitTags { + if !ValidTagRegex.MatchString(tag) { + log.Warnf("Rejected user-specified tag '%s' since it doesn't match regexp '%v'", + tag, ValidTagRegex) + } else { + validatedTags = append(validatedTags, tag) + } + } + + if len(validatedTags) > 0 { + return strings.Join(validatedTags, ";") + } + + return "" +} + +// tryEnterRootNamespaces tries to enter PID 1's UTS and network namespaces. +// It returns the file descriptor associated to each, or -1 if the namespace cannot be entered. +func tryEnterRootNamespaces() (utsFD, netFD int) { + netFD, err := pfnamespaces.EnterNamespace(1, "net") + if err != nil { + log.Errorf( + "Unable to enter root network namespace, host metadata may be incorrect: %v", err) + netFD = -1 + } + + utsFD, err = pfnamespaces.EnterNamespace(1, "uts") + if err != nil { + log.Errorf("Unable to enter root UTS namespace, host metadata may be incorrect: %v", err) + utsFD = -1 + } + + return utsFD, netFD +} diff --git a/hostmetadata/host/host_test.go b/hostmetadata/host/host_test.go new file mode 100644 index 00000000..10994dd3 --- /dev/null +++ b/hostmetadata/host/host_test.go @@ -0,0 +1,137 @@ +/* + * 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 host + +import ( + "net" + "os" + "sort" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/otel-profiling-agent/config" + "github.com/elastic/otel-profiling-agent/libpf" +) + +const ( + tags = "foo;bar;this-tag-should-be-dropped-!;baz;1.2.3.4;key:value;a_b_c" + validatedTags = "foo;bar;baz;1.2.3.4;key:value;a_b_c" +) + +func TestValidateTags(t *testing.T) { + tests := map[string]string{ + tags: validatedTags, + "": "", + } + + for testTags, testValidatedTags := range tests { + vTags := ValidateTags(testTags) + if vTags != testValidatedTags { + t.Errorf("validated tags %s != %s", vTags, testValidatedTags) + } + } +} + +func TestAddMetadata(t *testing.T) { + err := config.SetConfiguration(&config.Config{ + ProjectID: 42, + CacheDirectory: ".", + SecretToken: "secret", + ValidatedTags: validatedTags}) + if err != nil { + t.Fatalf("failed to set temporary config: %s", err) + } + + // This tests checks that common metadata keys are populated + metadataMap := make(map[string]string) + + // Ignore errors because collection may fail in unit tests. However, we check the contents of + // 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) + } + + ip, found := metadataMap[KeyIPAddress] + if !found { + t.Fatalf("no IP address") + } + + 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 procVersion != sanitizeString(expectedProcVersion) { + t.Fatalf("wrong kernel_proc_version, expected %v, got %v", procVersion, expectedProcVersion) + } + + _, found = metadataMap[KeyKernelVersion] + if !found { + t.Fatalf("no kernel version") + } + + // 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) + } + } + + cacheKey := key(keyCPUCacheL1d) + cache, ok := metadataMap[cacheKey] + assert.True(t, ok) + assert.NotEmpty(t, cache) + + cacheSockets, ok := metadataMap[keySocketID(cacheKey)] + assert.True(t, ok) + assert.NotEmpty(t, cacheSockets) + assert.True(t, cacheSockets[0] == '0', + "expected '0' at start of '%v'", cacheSockets) + sids := strings.Split(cacheSockets, ",") + socketIDs := libpf.MapSlice(sids, func(a string) int { + n, _ := strconv.Atoi(a) + return n + }) + assert.True(t, sort.IntsAreSorted(socketIDs), + "expected '%v' to be numerically sorted", sids) +} diff --git a/hostmetadata/hostmetadata.json b/hostmetadata/hostmetadata.json new file mode 100644 index 00000000..eb6fca21 --- /dev/null +++ b/hostmetadata/hostmetadata.json @@ -0,0 +1,619 @@ +[ + { + "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.go b/hostmetadata/instance/common.go new file mode 100644 index 00000000..6c66d53d --- /dev/null +++ b/hostmetadata/instance/common.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 instance + +import ( + "bufio" + "bytes" + "strings" +) + +// Enumerate converts a string response from a metadata service into a list of elements +func Enumerate(payload string) []string { + result := make([]string, 0) + s := bufio.NewScanner(bytes.NewBufferString(payload)) + for s.Scan() { + line := strings.TrimSuffix(strings.TrimSpace(s.Text()), "/") + // In case the response has empty lines we discard them + if line != "" { + result = append(result, line) + } + } + return result +} diff --git a/hostmetadata/instance/common_test.go b/hostmetadata/instance/common_test.go new file mode 100644 index 00000000..7245ebfe --- /dev/null +++ b/hostmetadata/instance/common_test.go @@ -0,0 +1,74 @@ +/* + * 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 instance + +import ( + "reflect" + "testing" +) + +func TestEnumerate(t *testing.T) { + r := Enumerate("") + if !reflect.DeepEqual([]string{}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("\n") + if !reflect.DeepEqual([]string{}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("\n\n") + if !reflect.DeepEqual([]string{}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("\nhello\n") + if !reflect.DeepEqual([]string{"hello"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("\nhello/\n") + if !reflect.DeepEqual([]string{"hello"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("hi\nhello/\n") + if !reflect.DeepEqual([]string{"hi", "hello"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("hi\nhello/\n\nbye") + if !reflect.DeepEqual([]string{"hi", "hello", "bye"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("\nbye") + if !reflect.DeepEqual([]string{"bye"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("hello/\n") + if !reflect.DeepEqual([]string{"hello"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("hello/") + if !reflect.DeepEqual([]string{"hello"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("\nhello/ \n") + if !reflect.DeepEqual([]string{"hello"}, r) { + t.Fatalf("unexpected result: %#v", r) + } + + r = Enumerate("hi\n \nbye") + if !reflect.DeepEqual([]string{"hi", "bye"}, r) { + t.Fatalf("unexpected result: %#v", r) + } +} diff --git a/hostmetadata/instance/instance.go b/hostmetadata/instance/instance.go new file mode 100644 index 00000000..21fd4743 --- /dev/null +++ b/hostmetadata/instance/instance.go @@ -0,0 +1,32 @@ +/* + * 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 instance provides functionality common to cloud providers, +// including synthetic cloud "instance" metadata. +package instance + +import "strings" + +const ( + // Prefix for synthetic instance metadata (used by azure, gce and ec2) + instancePrefix = "instance:" + + KeyPublicIPV4s = "public-ipv4s" + KeyPrivateIPV4s = "private-ipv4s" + + // Only Azure supports IPV6 + + KeyPublicIPV6s = "public-ipv6s" + KeyPrivateIPV6s = "private-ipv6s" +) + +func AddToResult(metadata map[string][]string, result map[string]string) { + for k, v := range metadata { + if len(v) > 0 { + result[instancePrefix+k] = strings.Join(v, ",") + } + } +} diff --git a/interpreter/hotspot/hotspot.go b/interpreter/hotspot/hotspot.go new file mode 100644 index 00000000..bbb55ff9 --- /dev/null +++ b/interpreter/hotspot/hotspot.go @@ -0,0 +1,1935 @@ +/* + * 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 + +// Java HotSpot Unwinder support code (works also with Scala using HotSpot) + +// nolint:lll +// The code here and in hotspot_tracer.ebpf.c is based on the Java Serviceability Agent (SA) code, +// and the Java DTrace helper code (libjvm_db). Additional insight is taken from +// https://github.com/jvm-profiling-tools/async-profiler/ unwinding parts, as well as various other +// online resources. +// +// Hotspot libjvm.so provides several tables of introspection data (such as the C++ class field +// offsets, struct sizes, etc.). These tables are accessed using several sets of exported symbols. +// The data from these tables is read and used to introspect the in-process JVM data structures. +// However, some additional assumptions are done e.g. the data type of each field, and the some +// aspects of the Interpreter stack layout. Some documentation about this introspection data is +// available at: +// https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/hotspot/share/runtime/vmStructs.hpp +// https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/hotspot/share/runtime/vmStructs.cpp +// +// The main references are available at (libjvm_db.c being the main source): +// https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/java.base/solaris/native/libjvm_db/libjvm_db.c +// https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/hotspot/cpu/x86/frame_x86.cpp +// https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/hotspot/os_cpu/linux_x86/thread_linux_x86.cpp +// https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/hotspot/share/prims/forte.cpp +// https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/profiler.cpp#L280 +// https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/stackFrame_x64.cpp +// https://docs.oracle.com/javase/specs/ +// +// In effect, the code here duplicates the Hotspot SA code in simple standalone manner that is +// portable across several JDK versions. Additional handling of certain states is done based +// on external resources, so this code should be faster, more portable and accurate than +// the Hotspot SA, or other tools. +// +// Notes about JDK changes (other than differences in introspection data values) which affect +// unwinding (incomplete list). The list items are changes done between the release major versions. +// +// JDK7 - Tested ok +// - renamed multiple C++ class names: methodOopDesc -> Method, constMethodOopDesc -> ConstMethod, etc. +// - due to the above some pointers are not including the sizeof OopDesc, and need to be explicitly added +// - InstanceKlass._source_file_name (Symbol*) -> _source_file_name_index +// - nmethod._oops_offset renamed to _metadata_offset +// JDK8 - Tested ok +// - Interpreter stack frame layout changed (BCP offset by one machine word) +// - CodeBlob: _code_offset separated to _code_begin and _code_end +// - CodeCache: _heap -> _heaps, global CodeCache boundaries added +// - CompiledMethod: split off from nmethod +// - nmethod._scopes_data_offset -> CompiledMethod._scopes_data_begin +// - nmethod._method -> CompiledMethod._method +// - nmethod._deoptimize_offset -> CompiledMethod._deopt_handler_begin +// - Modules introduced (but no introspection data to access those) +// JDK9 - Tested ok +// JDK10 - Tested ok +// JDK11 - Reference, works +// - Symbol.{_length, _refcount} merged to Symbol._length_and_refcount +// JDK12 - Tested ok +// - CompiledMethod smaller, nmethod shifted, works OK +// JDK13 - Tested ok +// - nmethod smaller, some members shifted, works OK +// JDK14 - Tested ok +// - InstanceKlass.Source_file_name_index moved to ConstantPool +// JDK15 - Tested ok +// - GenericGrowableArray renamed to GrowableArrayBase +// JDK16 - Tested ok +// JDK17 - Tested ok +// JDK18 - Tested ok +// JDK19 - Tested ok +// - Excluding zero byte from UNSIGNED5 encoding output +// JDK20 - 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. +// This causes the functions to not be in the Java CodeCache and be invisible for the unwinder. +// +// NOTE: Compressed oops is a feature of HotSpot that reduces the size of pointers to Java objects +// on the heap in many cases (see https://wiki.openjdk.org/display/HotSpot/CompressedOops for more +// details). It is turned on by default for heap sizes smaller than 32GB. However, the unwinder +// is not affected by compressed oops as we don't process objects on the Java heap. +// +// The approach is that code here will read the introspection data tables once for each JVM file +// and configure the important field offsets to eBPF via HotspotProcInfo. The eBPF code will then +// have enough data to unwind any HotSpot function via the JVM debug data. On each frame, the eBPF +// code will store four variables: 1. frame subtype, 2. a pointer, 3. a cookie, and 4. rip/bcp +// delta. The pointer is nmethod* (for JIT frames) or Method* (for Interpreted frames). +// +// Once the frame is received in this module's Symbolize function, the code will read additional +// data from the target process at given pointer. The "cookie" is also used to verify that the data +// at given pointer is still describing the same method as during the eBPF capture time. The read +// data is then further parsed, pointers there are followed by reading additional data as needed. +// Caching is done where possible to avoid reads from the target process via syscalls to reduce CPU +// overhead. Finally there should be enough data to produce symbolized frames. +// +// The above approach is selected because the process of symbolizing a JVM Method requires unbounded +// loops to parse the lineNumber tables and cannot be done in the eBPF code. The implication is that +// the frame values are specific to the process (as it has a pointer in it). Meaning the Trace IDs +// will be different if there's multiple VM instances running same Java application. But the overhead +// is not huge here. This also has the implication that in the very unlikely even of two different +// JVM instances producing identical trace (highly unlikely due to ASLR and the address cookie) the +// count aggregation might incorrectly produce wrong expansion. However, it's more likely that there +// 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" + "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{} +) + +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 +} + +// Loader is the main function for ProcessManager to recognize and hook the HotSpot +// libjvm for enabling JVM unwinding and symbolization. +func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + if !libjvmRegex.MatchString(info.FileName()) { + return nil, nil + } + + 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 +} diff --git a/interpreter/hotspot/hotspot_test.go b/interpreter/hotspot/hotspot_test.go new file mode 100644 index 00000000..44804745 --- /dev/null +++ b/interpreter/hotspot/hotspot_test.go @@ -0,0 +1,132 @@ +/* + * 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/stubs.go b/interpreter/hotspot/stubs.go new file mode 100644 index 00000000..15556938 --- /dev/null +++ b/interpreter/hotspot/stubs.go @@ -0,0 +1,269 @@ +/* + * 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" + "fmt" + "runtime" + "sort" + "strings" + + "github.com/elastic/otel-profiling-agent/debug/log" + "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/support" + aa "golang.org/x/arch/arm64/arm64asm" +) + +// nextAligned aligns a pointer up, to the next multiple of align. +func nextAligned(ptr libpf.Address, align uint64) libpf.Address { + return (ptr + libpf.Address(align)) & ^(libpf.Address(align) - 1) +} + +// StubRoutine marks a logical function within the StubRoutines blob. +type StubRoutine struct { + name string + start, end libpf.Address +} + +// findStubBounds heuristically determines the bounds of individual functions +// within the larger StubRoutines blobs. We receive pointers for most of the +// stubs from VM structs / JVMCI VM structs, but not the lengths. +// +// This function first collects all routines and sorts them by start address. +// The start of the next stub is then taken as the maximum length of the current +// stub. This works great for most cases, but some functions are missing in VM +// structs (and would thus be assigned to the previous stub), and the last +// function doesn't have anything following it to serve as a boundary. +// +// To handle these edge-cases, we additionally do a sweep for NOP instructions +// that are used as padding between subroutines. One might be inclined to rely +// on this NOP heuristic only, but it's not sufficient alone either: the +// previous stub function might have the perfect length for the next one to +// not need alignment. Also in some cases the JVM devs omitted/forgot to insert +// the padding. The two heuristics combined, however, yield reliable results. +func findStubBounds(vmd *hotspotVMData, bias libpf.Address, + rm remotememory.RemoteMemory) []StubRoutine { + const CodeAlign = 64 + const MaxStubLen = 8 * 1024 + + stubs := make([]StubRoutine, 0, 64) + for field, addr := range vmd.vmStructs.StubRoutines.CatchAll { + if strings.Contains(field, "_table_") { + continue + } + + // Not all stubs are generated for all architectures. + entry := rm.Ptr(addr + bias) + if entry == 0 { + continue + } + + stubs = append(stubs, StubRoutine{ + name: strings.TrimPrefix(field, "_"), + start: entry, + end: 0, // filled in later + }) + } + + sort.Slice(stubs, func(i, j int) bool { + if stubs[i].start != stubs[j].start { + return stubs[i].start < stubs[j].start + } + + // Secondary ordering by name to ensure that we produce deterministic + // results even in the presence of stub aliases (same start address). + return stubs[i].name < stubs[j].name + }) + + filtered := make([]StubRoutine, 0, len(stubs)) + for i := 0; i < len(stubs); i++ { + cur := &stubs[i] + + // Some stubs re-use the code from another stub. Skip elements until + // we detected the next stub that doesn't occupy the same address. + for i < len(stubs) { + if i != len(stubs)-1 { + // Beginning of next element marks the maximum length of the + // previous one. + next := &stubs[i+1] + cur.end = next.start + } else { + // Last element: assume max length and let the disassembler + // heuristic below deal with that case. + cur.end = cur.start + MaxStubLen - 1 + } + + if cur.start == cur.end { + i++ + } else { + break + } + } + + // Sweep for stub function boundary. + heuristicEnd := libpf.Address(0) + NopHeuristic: + for p := nextAligned(cur.start, CodeAlign); p < cur.start+MaxStubLen; p += CodeAlign { + const NopARM4 = 0xD503201F + const NopAMD64 = 0x90 + + block := make([]byte, CodeAlign) + if err := rm.Read(p-CodeAlign, block); err != nil { + continue + } + + // Last function in each stub is followed by zeros. + if libpf.SliceAllEqual(block, 0) { + heuristicEnd = p + break NopHeuristic + } + + // Other functions are separated by NOPs. + switch runtime.GOARCH { + case "arm64": //nolint:goconst + if binary.LittleEndian.Uint32(block[len(block)-4:]) == NopARM4 { + heuristicEnd = p + break NopHeuristic + } + case "amd64": + if block[len(block)-1] == NopAMD64 { + heuristicEnd = p + break NopHeuristic + } + default: + panic("unexpected architecture") + } + } + + // Pick the minimum of both heuristics as length. + if heuristicEnd != 0 { + cur.end = min(cur.end, heuristicEnd) + } + + if cur.end-cur.start > MaxStubLen { + log.Debugf("Unable to determine length for JVM stub %s", cur.name) + continue + } + + filtered = append(filtered, *cur) + } + + return filtered +} + +// analyzeStubArm64 disassembles the first 16 instructions of an ARM64 stub in +// an attempt to detect whether it has a frame or needs an SP offset. +// +// Examples of cases currently handled by this function: +// +// Stack frame setup (checkcast_arraycopy_uninit): +// >>> STP X29, X30, [SP,#-0x10]! +// >>> MOV X29, SP +// +// Stack alloc without frame via mutating STP variant (sha256_implCompress): +// >>> STP D8, D9, [SP,#-0x20]! +// +// Stack alloc with SUB after a few instructions (ghash_processBlocks_wide): +// >>> CMP X3, #8 +// >>> B.LT loc_4600 +// >>> SUB SP, SP, #0x40 +func analyzeStubArm64(rm remotememory.RemoteMemory, addr libpf.Address) ( + hasFrame bool, spOffs int64, err error) { + code := make([]byte, 64) + if err := rm.Read(addr, code); err != nil { + return false, 0, err + } + +Outer: + for offs := 0; offs < len(code); offs += 4 { + insn, err := aa.Decode(code[offs : offs+4]) + if err != nil { + return false, 0, fmt.Errorf("failed to decode instruction: %v", err) + } + + const SP = aa.RegSP(aa.SP) + + switch insn.Op { + case aa.STP: + if insn.Args[0] == aa.X29 && insn.Args[1] == aa.X30 { + // Assume this is a frame pointer setup. + return true, 0, nil + } + + if arg, ok := insn.Args[2].(aa.MemImmediate); ok { + if arg.Base != SP { + continue + } + if arg.Mode != aa.AddrPostIndex && arg.Mode != aa.AddrPreIndex { + continue + } + imm, ok := armhelpers.DecodeImmediate(arg) + if !ok { + continue + } + + spOffs += int64(imm) + } + case aa.SUB: + for _, arg := range insn.Args[:2] { + if arg, ok := arg.(aa.RegSP); !ok || arg != SP { + continue Outer + } + } + imm, ok := armhelpers.DecodeImmediate(insn.Args[2]) + if !ok { + continue + } + + spOffs -= int64(imm) + } + } + + return false, spOffs, nil +} + +// jitAreaForStubArm64 synthesizes a jitArea for an ARM64 stub routine. +// +// We currently don't make any attempts to generate extra areas for the pro- +// and epilogues of the functions and (incorrectly) assume the SP deltas for +// the duration of the whole function. We expect it to be sufficiently rare +// that sampling catches the pro/epilogues that it isn't really worth special +// casing this any further. +func jitAreaForStubArm64(stub *StubRoutine, heap *jitArea, + rm remotememory.RemoteMemory) (jitArea, error) { + var hasFrame bool + var spOffs int64 + if stub.name == "call_stub_return_address" { + // Special-case: this is not an actual individual stub function, + // but rather a pointer into the middle of the call stub. + hasFrame = true + } else { + var err error + hasFrame, spOffs, err = analyzeStubArm64(rm, stub.start) + if err != nil { + return jitArea{}, fmt.Errorf("failed to analyze stub: %v", err) + } + } + + tsid := heap.tsid | 1<FindPage() for large page mappings +// * Page::FromAddress() for standard heap page mappings +// see: v8/src/objects/code.cc,h +// - if Code object is known, can use instructions_start etc +// for extracting the inlined functions from compiled code: +// see: v8/src/deoptimizer/deoptimizer.cc, TranslatedState class and helpers +// - TranslatedState::Init for decoding + +import ( + "bytes" + "fmt" + "hash/fnv" + "io" + "reflect" + "regexp" + "sort" + "strings" + "sync/atomic" + "unsafe" + + 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/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" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/support" + "go.uber.org/multierr" +) + +// #include "../../support/ebpf/types.h" +// #include "../../support/ebpf/v8_tracer.h" +import "C" + +const ( + // Use build-time constants for the HeapObject/SMI Tags for code size and speed. + // They are unlikely to change, and likely require larger modifications on change. + SmiTag = C.SmiTag + SmiTagMask = C.SmiTagMask + SmiTagShift = C.SmiTagShift + SmiValueShift = C.SmiValueShift + HeapObjectTag = C.HeapObjectTag + HeapObjectTagMask = C.HeapObjectTagMask + + // The largest possible identifier for V8 frame type (marker) + MaxFrameType = 64 + + // The base address for Trace frame addressOrLine of native code. + // This is make sure that if same function gets both bytecode based and + // native frames, that we don't end up generating conflicting symbolization + // for them as native frames use real line number, and bytecode uses + // bytecode offset as the line number. + nativeCodeBaseAddress = 0x100000000 + + // The maximum fixed table size we accept to read. An arbitrarily selected + // value to avoid huge malloc that could cause OOM crash. + maximumFixedTableSize = 512 * 1024 + + // lruSourceFileCacheSize is the LRU size for caching source files for an interpreter. + // This should reflect the number of hot source files that are seen often in a trace. + lruSourceFileCacheSize = 128 + + // lruMapTypeCacheSize is the LRU size for caching the Map.InstanceType field. + lruMapTypeCacheSize = 32 + + // The native pointer size in bytes for 64-bit architectures + pointerSize = 8 +) + +var ( + // regex for the interpreter executable + v8Regex = regexp.MustCompile(`^(?:.*/)?node$`) + + // The FileID used for V8 stub frames + v8StubsFileID = libpf.NewFileID(0x578b, 0x1d) + + // the source file entry for unknown code blobs + unknownSource = &v8Source{fileName: interpreter.UnknownSourceFile} + + // compiler check to make sure the needed interfaces are satisfied + _ interpreter.Data = &v8Data{} + _ interpreter.Instance = &v8Instance{} +) + +// nolint:lll +type v8Data struct { + // vmStructs reflects the V8 internal class names and the offsets of named fields. + // The V8 name is in the tag 'name' if it differs from our Go name. + vmStructs struct { + Fixed struct { + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#83 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/include/v8-internal.h#38 + HeapObjectTagMask uint32 + SmiTagMask uint32 + HeapObjectTag uint16 + SmiTag uint16 `zero:""` + SmiShiftSize uint16 + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/include/v8-internal.h#261 + FirstNonstringType uint16 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/include/v8-internal.h#219 + StringEncodingMask uint16 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/instance-type.h#26 + StringRepresentationMask uint16 + SeqStringTag uint16 `zero:""` + ConsStringTag uint16 + OneByteStringTag uint16 + TwoByteStringTag uint16 `zero:""` + SlicedStringTag uint16 + ThinStringTag uint16 + + // https://chromium.googlesource.com/v8/v8.git/+/ca3ef3fdf13f75475bcc964d7d79f5f7c66ea312/tools/gen-postmortem-metadata.py#82 + FirstJSFunctionType uint16 + LastJSFunctionType uint16 + } `name:""` + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#182 + FramePointer struct { + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#70 + Function uint8 `name:"off_fp_function"` + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#118 + Context uint8 `name:"off_fp_context"` + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#332 + BytecodeArray uint8 `name:"off_fp_bytecode_array"` + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#334 + BytecodeOffset uint8 `name:"off_fp_bytecode_offset"` + } `name:""` + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#195 + ScopeInfoIndex struct { + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.h#258 + FirstVars uint8 `name:"scopeinfo_idx_first_vars"` + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.h#247 + NContextLocals uint8 `name:"scopeinfo_idx_ncontextlocals"` + } `name:""` + + // submitted upstream: https://chromium-review.googlesource.com/c/v8/v8/+/3902524 + DeoptimizationDataIndex struct { + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#912 + InlinedFunctionCount uint8 `name:"DeoptimizationDataInlinedFunctionCountIndex"` + LiteralArray uint8 `name:"DeoptimizationDataLiteralArrayIndex"` + SharedFunctionInfo uint8 `name:"DeoptimizationDataSharedFunctionInfoIndex"` + InliningPositions uint8 `name:"DeoptimizationDataInliningPositionsIndex"` + } `name:""` + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code-kind.h#18 + CodeKind struct { + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#526 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.5.2/tools/gen-postmortem-metadata.py#94 + FieldMask uint32 `name:"CodeKindFieldMask" zero:""` + FieldShift uint8 `name:"CodeKindFieldShift" zero:""` + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code-kind.h#18 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.5.2/tools/gen-postmortem-metadata.py#101 + Baseline uint8 `name:"CodeKindBaseline"` + } `name:""` + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#341 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frames.h#95 + FrameType struct { + ArgumentsAdaptorFrame uint8 + BaselineFrame uint8 + BuiltinContinuationFrame uint8 + BuiltinExitFrame uint8 + BuiltinFrame uint8 + CWasmEntryFrame uint8 + ConstructEntryFrame uint8 + ConstructFrame uint8 + EntryFrame uint8 + ExitFrame uint8 + InternalFrame uint8 + InterpretedFrame uint8 + JavaScriptBuiltinContinuationFrame uint8 + JavaScriptBuiltinContinuationWithCatchFrame uint8 + JavaScriptFrame uint8 + JsToWasmFrame uint8 + NativeFrame uint8 + OptimizedFrame uint8 + StubFrame uint8 + WasmCompileLazyFrame uint8 + WasmCompiledFrame uint8 + WasmExitFrame uint8 + WasmInterpreterEntryFrame uint8 + WasmToJsFrame uint8 + } `name:"frametype"` + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#709 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/instance-type.h#75 + Type struct { + BaselineData uint16 `name:"BaselineData__BASELINE_DATA_TYPE" zero:""` + ByteArray uint16 `name:"ByteArray__BYTE_ARRAY_TYPE"` + BytecodeArray uint16 `name:"BytecodeArray__BYTECODE_ARRAY_TYPE"` + Code uint16 `name:"Code__CODE_TYPE"` + FixedArray uint16 `name:"FixedArray__FIXED_ARRAY_TYPE"` + WeakFixedArray uint16 `name:"WeakFixedArray__WEAK_FIXED_ARRAY_TYPE"` + JSFunction uint16 `name:"JSFunction__JS_FUNCTION_TYPE"` + Map uint16 `name:"Map__MAP_TYPE"` + Script uint16 `name:"Script__SCRIPT_TYPE"` + ScopeInfo uint16 `name:"ScopeInfo__SCOPE_INFO_TYPE"` + SharedFunctionInfo uint16 `name:"SharedFunctionInfo__SHARED_FUNCTION_INFO_TYPE"` + } `name:"type"` + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/heap-object.tq#7 + HeapObject struct { + Map uint16 `name:"map__Map" zero:""` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/map.tq#37 + Map struct { + InstanceType uint16 `name:"instance_type__uint16_t"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/fixed-array.tq#7 + FixedArrayBase struct { + Length uint16 `name:"length__SMI"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/fixed-array.tq#14 + FixedArray struct { + Data uint16 `name:"data__uintptr_t"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#10 + String struct { + Length uint16 `name:"length__int32_t"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#108 + SeqOneByteString struct { + Chars uint16 `name:"chars__char"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#114 + SeqTwoByteString struct { + Chars uint16 `name:"chars__char"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#37 + ConsString struct { + First uint16 `name:"first__String"` + Second uint16 `name:"second__String"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#129 + ThinString struct { + Actual uint16 `name:"actual__String"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/js-function.tq#23 + JSFunction struct { + Code uint16 `name:"code__Code,code__Tagged_Code_"` + SharedFunctionInfo uint16 `name:"shared__SharedFunctionInfo"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#467 + Code struct { + DeoptimizationData uint16 `name:"deoptimization_data__FixedArray,deoptimization_data__Tagged_FixedArray_"` + SourcePositionTable uint16 `name:"source_position_table__ByteArray,source_position_table__Tagged_ByteArray_"` + InstructionStart uint16 `name:"instruction_start__uintptr_t,instruction_start__Address"` + InstructionSize uint16 `name:"instruction_size__int"` + Flags uint16 `name:"flags__uint32_t"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/shared-function-info.tq#57 + SharedFunctionInfo struct { + NameOrScopeInfo uint16 `name:"name_or_scope_info__Object,name_or_scope_info__Tagged_Object_"` + FunctionData uint16 `name:"function_data__Object,function_data__Tagged_Object_"` + ScriptOrDebugInfo uint16 `name:"script_or_debug_info__Object,script_or_debug_info__HeapObject,script__Tagged_HeapObject_"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/shared-function-info.tq#19 + BaselineData struct { + Data uint16 `name:"data__Object" zero:""` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.tq#7 + BytecodeArray struct { + SourcePositionTable uint16 `name:"source_position_table__Object,source_position_table__Tagged_HeapObject_"` + Data uint16 `name:"data__uintptr_t"` + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.h#41 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#39 + // The bool type indicates it's a parent relation type symbol. + ScopeInfo struct { + HeapObject bool + } + + // class DeoptimizationLiteralArray introduced in V8 9.8.23 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.2.154.1/src/objects/code.h#1090 + DeoptimizationLiteralArray struct { + WeakFixedArray bool + } + + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/script.tq#18 + Script struct { + Name uint16 `name:"name__Object"` + LineEnds uint16 `name:"line_ends__Object"` + Source uint16 `name:"source__Object"` + } + } + + // snapshotRange is the LOAD segment area where V8 Snapshot code blob is + snapshotRange libpf.Range + + // version contains the V8 version + version uint32 + + // bytecodeSizes contains the V8 bytecode length data + bytecodeSizes []byte + + // bytecodeCount is the number of bytecode opcodes + bytecodeCount uint8 + + // frametypeToID caches frametype's to a hash used as its identifier + frametypeToID [MaxFrameType]libpf.AddressOrLineno + + // externStubID caches the hash of stub frame to indicate external file + externalStubID libpf.AddressOrLineno +} + +type v8Instance struct { + interpreter.InstanceStubs + + // Symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + + d *v8Data + rm remotememory.RemoteMemory + + // addrToString maps a V8 string object address to a Go string literal + addrToString *freelru.LRU[libpf.Address, string] + addrToSFI *freelru.LRU[libpf.Address, *v8SFI] + addrToCode *freelru.LRU[libpf.Address, *v8Code] + addrToSource *freelru.LRU[libpf.Address, *v8Source] + addrToType *freelru.LRU[libpf.Address, uint16] + + // mappings is indexed by the Mapping to its generation + mappings map[process.Mapping]uint32 + // prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation + prefixes map[lpm.Prefix]uint32 + // mappingGeneration is the current generation (so old entries can be pruned) + mappingGeneration uint32 +} + +// v8Source caches the data we need from V8 class Source +type v8Source struct { + lineTable []uint32 + fileName string +} + +// v8Code caches the data we need from V8 class Code +type v8Code struct { + sfi *v8SFI + isBaseline bool + codeDeltaToPosition map[uint32]sourcePosition + codePositionTable []byte + inliningSFIs []byte + inliningPositions []byte + cookie uint32 +} + +// v8SFI caches the data we need from V8 class SharedFunctionInfo +type v8SFI struct { + source *v8Source + bytecodeDeltaSeen libpf.Set[uint32] + bytecodePositionTable []byte + bytecode []byte + funcName string + funcID libpf.FileID + funcStartLine libpf.SourceLineno + funcStartPos int + funcEndPos int + bytecodeLength uint32 +} + +func (i *v8Instance) Detach(ebpf interpreter.EbpfHandler, pid libpf.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, + fmt.Errorf("failed to remove page 0x%x/%d: %v", + prefix.Key, prefix.Length, err2)) + } + } + if err != nil { + return fmt.Errorf("failed to detach v8Instance from PID %d: %v", + pid, err) + } + return nil +} + +func (i *v8Instance) SynchronizeMappings(ebpf interpreter.EbpfHandler, + _ reporter.SymbolReporter, pr process.Process, mappings []process.Mapping) error { + pid := pr.PID() + i.mappingGeneration++ + for idx := range mappings { + m := &mappings[idx] + if !m.IsExecutable() || !m.IsAnonymous() { + continue + } + + _, exists := i.mappings[*m] + i.mappings[*m] = i.mappingGeneration + if exists { + continue + } + + // Just assume all anonymous and executable mappings are V8 for now + log.Debugf("Enabling V8 for %#x/%#x", m.Vaddr, m.Length) + + prefixes, err := lpm.CalculatePrefixList(m.Vaddr, m.Vaddr+m.Length) + if err != nil { + return fmt.Errorf("new anonymous mapping lpm failure %#x/%#x", m.Vaddr, m.Length) + } + + for _, prefix := range prefixes { + if _, exists := i.prefixes[prefix]; exists { + i.prefixes[prefix] = i.mappingGeneration + continue + } + err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindV8, 0, 0) + if err != nil { + return err + } + i.prefixes[prefix] = i.mappingGeneration + } + } + + // Remove prefixes not seen + for prefix, generation := range i.prefixes { + if generation == i.mappingGeneration { + continue + } + _ = ebpf.DeletePidInterpreterMapping(pid, prefix) + delete(i.prefixes, prefix) + } + for m, generation := range i.mappings { + if generation == i.mappingGeneration { + continue + } + log.Debugf("Disabling V8 for %#x/%#x", m.Vaddr, m.Length) + delete(i.mappings, m) + } + + return nil +} + +func (i *v8Instance) GetAndResetMetrics() ([]metrics.Metric, error) { + addrToStringStats := i.addrToString.GetAndResetStatistics() + addrToSFIStats := i.addrToSFI.GetAndResetStatistics() + addrToCodeStats := i.addrToCode.GetAndResetStatistics() + addrToSourceStats := i.addrToSource.GetAndResetStatistics() + + return []metrics.Metric{ + { + ID: metrics.IDV8SymbolizationSuccess, + Value: metrics.MetricValue(i.successCount.Swap(0)), + }, + { + ID: metrics.IDV8SymbolizationFailure, + Value: metrics.MetricValue(i.failCount.Swap(0)), + }, + { + ID: metrics.IDV8AddrToStringHit, + Value: metrics.MetricValue(addrToStringStats.Hit), + }, + { + ID: metrics.IDV8AddrToStringMiss, + Value: metrics.MetricValue(addrToStringStats.Miss), + }, + { + ID: metrics.IDV8AddrToStringAdd, + Value: metrics.MetricValue(addrToStringStats.Added), + }, + { + ID: metrics.IDV8AddrToStringDel, + Value: metrics.MetricValue(addrToStringStats.Deleted), + }, + { + ID: metrics.IDV8AddrToSFIHit, + Value: metrics.MetricValue(addrToSFIStats.Hit), + }, + { + ID: metrics.IDV8AddrToSFIMiss, + Value: metrics.MetricValue(addrToSFIStats.Miss), + }, + { + ID: metrics.IDV8AddrToSFIAdd, + Value: metrics.MetricValue(addrToSFIStats.Added), + }, + { + ID: metrics.IDV8AddrToSFIDel, + Value: metrics.MetricValue(addrToSFIStats.Deleted), + }, + { + ID: metrics.IDV8AddrToFuncHit, + Value: metrics.MetricValue(addrToCodeStats.Hit), + }, + { + ID: metrics.IDV8AddrToFuncMiss, + Value: metrics.MetricValue(addrToCodeStats.Miss), + }, + { + ID: metrics.IDV8AddrToFuncAdd, + Value: metrics.MetricValue(addrToCodeStats.Added), + }, + { + ID: metrics.IDV8AddrToFuncDel, + Value: metrics.MetricValue(addrToCodeStats.Deleted), + }, + { + ID: metrics.IDV8AddrToSourceHit, + Value: metrics.MetricValue(addrToSourceStats.Hit), + }, + { + ID: metrics.IDV8AddrToSourceMiss, + Value: metrics.MetricValue(addrToSourceStats.Miss), + }, + { + ID: metrics.IDV8AddrToSourceAdd, + Value: metrics.MetricValue(addrToSourceStats.Added), + }, + { + ID: metrics.IDV8AddrToSourceDel, + Value: metrics.MetricValue(addrToSourceStats.Deleted), + }, + }, nil +} + +// v8Ver encodes the x.y.z version to single uint32 +func v8Ver(x, y, z uint32) uint32 { + return (x << 24) + (y << 16) + z +} + +// isSMI tests if the machine word is a V8 SMI (SMall Integer) +func isSMI(val uint64) bool { + return val&SmiTagMask == SmiTag +} + +// decideSMI extracts the integer value from a SMI. Returns zero for bad tag. +func decodeSMI(val uint64) uint32 { + if !isSMI(val) { + return 0 + } + return uint32(val >> SmiValueShift) +} + +// isHeapObject tests if the given address is a valid tagged pointer +func isHeapObject(val libpf.Address) bool { + return val&HeapObjectTagMask == HeapObjectTag +} + +// calculateAndSymbolizeStubID calculates the hash for a given string, and symbolizes it. +func (i *v8Instance) calculateAndSymbolizeStubID(symbolizer interpreter.Symbolizer, + 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, "") + + return stubID +} + +// insertFrame inserts a V8 frame to libpf.Trace +func insertFrame(trace *libpf.Trace, fileID libpf.FileID, line libpf.AddressOrLineno) { + trace.AppendFrame(libpf.V8Frame, fileID, line) +} + +// symbolizeMarkerFrame symbolizes and adds to trace a V8 stub frame +func (i *v8Instance) symbolizeMarkerFrame(symbolizer interpreter.Symbolizer, marker uint64, + trace *libpf.Trace) error { + if marker >= MaxFrameType { + return fmt.Errorf("v8 tracer returned invalid marker: %d", marker) + } + + stubID := i.d.frametypeToID[marker] + if stubID == 0 { + name := "V8::UnknownFrame" + frameTypesType := reflect.TypeOf(&i.d.vmStructs.FrameType).Elem() + frameTypesValue := reflect.ValueOf(&i.d.vmStructs.FrameType).Elem() + for i := 0; i < frameTypesValue.NumField(); i++ { + if frameTypesValue.Field(i).Uint() == marker { + name = "V8::" + frameTypesType.Field(i).Name + break + } + } + stubID = i.calculateAndSymbolizeStubID(symbolizer, name) + i.d.frametypeToID[marker] = stubID + + log.Debugf("[%d] V8 marker %v is %s, stubID %x", len(trace.FrameTypes), + marker, name, stubID) + } + + insertFrame(trace, v8StubsFileID, stubID) + return nil +} + +// getObjectAddrAndType validates tagged pointer and reads its object tag. +// On return, the actual address and its type tag are returned, or an error. +func (i *v8Instance) getObjectAddrAndType(taggedPtr libpf.Address) (libpf.Address, uint16, error) { + vms := &i.d.vmStructs + if !isHeapObject(taggedPtr) { + return 0, 0, fmt.Errorf("%#x is not a tagged pointer", taggedPtr) + } + addr := taggedPtr &^ HeapObjectTagMask + taggedMapAddr := i.rm.Ptr(addr + libpf.Address(vms.HeapObject.Map)) + if taggedMapAddr == 0 || !isHeapObject(taggedMapAddr) { + return 0, 0, fmt.Errorf("object map for %#x is not a valid heap pointer", taggedPtr) + } + + if instanceType, ok := i.addrToType.Get(taggedMapAddr); ok { + return addr, instanceType, nil + } + + mapAddr := taggedMapAddr &^ HeapObjectTagMask + instanceType := i.rm.Uint16(mapAddr + libpf.Address(vms.Map.InstanceType)) + if instanceType != 0 { + i.addrToType.Add(taggedMapAddr, instanceType) + } + return addr, instanceType, nil +} + +// getTypedObject checks the object's type, and returns its address or error. +func (i *v8Instance) getTypedObject(taggedPtr libpf.Address, expectedType uint16) ( + libpf.Address, error) { + addr, tag, err := i.getObjectAddrAndType(taggedPtr) + if err != nil { + return 0, err + } + if tag != expectedType { + return 0, fmt.Errorf("%#x instance is %#x, but expected %#x", addr, tag, expectedType) + } + return addr, nil +} + +// readObjectPtr reads an object pointer, and parses it as a HeapObject pointer. +func (i *v8Instance) readObjectPtr(addr libpf.Address) (libpf.Address, uint16, error) { + return i.getObjectAddrAndType(i.rm.Ptr(addr)) +} + +// readTypedObjectPtr reads an object pointer and makes sure it is a HeapObject of expected type +func (i *v8Instance) readTypedObjectPtr(addr libpf.Address, expectedType uint16) ( + libpf.Address, error) { + addr, tag, err := i.readObjectPtr(addr) + if err != nil { + return 0, err + } + if tag != expectedType { + return 0, fmt.Errorf("%#x instance is %#x, but expected %#x", addr, tag, expectedType) + } + return addr, nil +} + +// extractString reads string from given address. If the object type tag is given, the pointer +// can be tagged or not. Zero tag can be used to first read and validate the string tag, in this +// case the pointer is expected to be in the tagged format. +// The extracted string can be large (e.g. entire files of source code), and is extracted in +// fragments. Some V8 string representations (e.g. ConsString) is naturally fragmented, but this +// code will also internally split long continuous string literals to fragments to avoid large +// memory usage. +func (i *v8Instance) extractString(ptr libpf.Address, tag uint16, cb func(string) error) error { + var err error + + vms := &i.d.vmStructs + if tag == 0 { + ptr, tag, err = i.getObjectAddrAndType(ptr) + if err != nil { + return err + } + } + + if tag >= vms.Fixed.FirstNonstringType { + return fmt.Errorf("not a string at %#x, tag is %#x", ptr, tag) + } + + switch tag & vms.Fixed.StringRepresentationMask { + case vms.Fixed.SeqStringTag: + length := i.rm.Uint32(ptr + libpf.Address(vms.String.Length)) + switch tag & vms.Fixed.StringEncodingMask { + case vms.Fixed.OneByteStringTag: + bufSz := uint32(16 * 1024) + if bufSz > length { + bufSz = length + } + buf := make([]byte, bufSz) + for offs := uint32(0); offs < length; offs += bufSz { + if length-offs < bufSz { + buf = buf[0 : length-offs] + } + err = i.rm.Read(ptr+ + libpf.Address(vms.SeqOneByteString.Chars)+ + libpf.Address(offs), + buf) + if err != nil { + return err + } + if err = cb(string(buf)); err != nil { + return err + } + } + case vms.Fixed.TwoByteStringTag: + return fmt.Errorf("two byte string not supported") + default: + return fmt.Errorf("unsupported encoding: %#x", tag) + } + case vms.Fixed.ConsStringTag: + if err = i.extractStringPtr(ptr+libpf.Address(vms.ConsString.First), + cb); err != nil { + return err + } + if err = i.extractStringPtr(ptr+libpf.Address(vms.ConsString.Second), + cb); err != nil { + return err + } + case vms.Fixed.ThinStringTag: + return i.extractStringPtr(ptr+libpf.Address(vms.ThinString.Actual), cb) + default: + return fmt.Errorf("unsupported string tag %#x", tag&vms.Fixed.StringRepresentationMask) + } + return nil +} + +func (i *v8Instance) extractStringPtr(ptr libpf.Address, cb func(string) error) error { + return i.extractString(i.rm.Ptr(ptr), 0, cb) +} + +// getString extracts and caches a small string object from given address. +func (i *v8Instance) getString(ptr libpf.Address, tag uint16) (string, error) { + taggedPtr := ptr | HeapObjectTag + if value, ok := i.addrToString.Get(taggedPtr); ok { + return value, nil + } + + str := "" + err := i.extractString(ptr, tag, func(fragment string) error { + // 1kB maximum for file, function and class names + if len(str)+len(fragment) >= 1024 { + return fmt.Errorf("string too long (at least %d+%d)", + len(str), len(fragment)) + } + str += fragment + return nil + }) + if err != nil { + return "", err + } + if str != "" && !libpf.IsValidString(str) { + return "", fmt.Errorf("invalid string at 0x%x", ptr) + } + + i.addrToString.Add(taggedPtr, str) + return str, nil +} + +// getStringPtr reads a V8 string pointer and dereferences it. +func (i *v8Instance) getStringPtr(ptr libpf.Address) (string, error) { + return i.getString(i.rm.Ptr(ptr), 0) +} + +// analyzeScopeInfo reads and heuristically analyzes V8 ScopeInfo data. It tries to +// extract the function name, and its start and end line. +func (i *v8Instance) analyzeScopeInfo(ptr libpf.Address) (name string, + startPos, endPos int, err error) { + vms := &i.d.vmStructs + var data libpf.Address + if vms.ScopeInfo.HeapObject { + // ScopeInfo is HeapObject based (since V8 9.1.71) + data = ptr + libpf.Address(vms.HeapObject.Map+pointerSize) + } else { + // ScopeInfo is FixedArray based (before V8 9.1.71) + data = ptr + libpf.Address(vms.FixedArray.Data) + } + + // The maximum number of 'slots' inspected. Based on assumptions on what + // data can be in the array before function name and line numbers. The exact + // number varies V8 version to version. Counting the actual slot number + // would require tracking lot of V8 bitfields that have changed a lot, see: + // nolint:lll + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.tq#50 + const numSlots = 16 + const slotSize = pointerSize + slotData := make([]byte, numSlots*slotSize) + if err = i.rm.Read(data, slotData); err != nil { + return "", 0, 0, nil + } + + // Skip reserved slots and the context locals + ndx := int(vms.ScopeInfoIndex.FirstVars) + ndx += 2 * int(decodeSMI(npsr.Uint64(slotData, + uint(vms.ScopeInfoIndex.NContextLocals)*slotSize))) + + prev := uint64(HeapObjectTag) + for ; ndx < numSlots; ndx++ { + cur := npsr.Uint64(slotData, uint(ndx*slotSize)) + if name == "" && isHeapObject(libpf.Address(cur)) { + // Just try getting the string ignoring errors and + // assume that first valid string is the function name + name, _ = i.getString(libpf.Address(cur), 0) + } + if isSMI(cur) && isSMI(prev) { + // Assume that two numbers (first one lower than the second) + // is the start/end position pair. This also follows after + // function name, so break when found. + startPos = int(decodeSMI(prev)) + endPos = int(decodeSMI(cur)) + if startPos < endPos { + return name, startPos, endPos, nil + } + } + prev = cur + } + return name, 0, 0, nil +} + +// readFixedTable reads the data of a FixedArray object. +func (i *v8Instance) readFixedTable(addr libpf.Address, itemSize, maxItems uint32) ([]byte, error) { + vms := &i.d.vmStructs + + numItems := decodeSMI(i.rm.Uint64(addr + libpf.Address(vms.FixedArrayBase.Length))) + if maxItems != 0 && numItems > maxItems { + numItems = maxItems + } + + size := numItems * itemSize + if size == 0 || size >= maximumFixedTableSize { + return nil, fmt.Errorf("fixed table size: %d", size) + } + + data := make([]byte, size) + err := i.rm.Read(addr+libpf.Address(vms.FixedArray.Data), data) + if err != nil { + return nil, fmt.Errorf("fixed table: %w", err) + } + + return data, nil +} + +// readFixedTablePtr read the data of a FixedArray object. +func (i *v8Instance) readFixedTablePtr(taggedPtr libpf.Address, tag uint16, + itemSize, maxItems uint32) ([]byte, error) { + addr, err := i.readTypedObjectPtr(taggedPtr, tag) + if err != nil { + return nil, err + } + return i.readFixedTable(addr, itemSize, maxItems) +} + +// getSource reads and caches needed V8 Source object data. +func (i *v8Instance) getSource(addr libpf.Address) (*v8Source, error) { + if value, ok := i.addrToSource.Get(addr); ok { + return value, nil + } + + vms := &i.d.vmStructs + + var err error + src := &v8Source{} + src.fileName, _ = i.getStringPtr(addr + libpf.Address(vms.Script.Name)) + if vms.Script.LineEnds != 0 { + // First read the LineEnds directly if available + var data []byte + data, err = i.readFixedTablePtr( + addr+libpf.Address(vms.Script.LineEnds), + vms.Type.FixedArray, 8, 0) + log.Debugf("Reading LineEnds: %d: %v", len(data), err) + if err == nil { + lines := make([]uint32, len(data)/8) + for i := 0; i < len(lines); i++ { + val := npsr.Uint64(data, uint(i*8)) + lines[i] = decodeSMI(val) + } + src.lineTable = lines + } + } + if src.lineTable == nil { + // Try reading the full source to calculate line ends + ends := make([]uint32, 0, 100) + prev := byte(0) + fragStart := 0 + err = i.extractStringPtr(addr+libpf.Address(vms.Script.Source), + func(fragment string) error { + for i := 0; i < len(fragment); i++ { + ch := fragment[i] + if ch == '\r' || (ch == '\n' && prev != '\r') { + ends = append(ends, uint32(fragStart+i)) + } + prev = ch + } + fragStart += len(fragment) + return nil + }) + log.Debugf("Reading Source: %d lines: %v", len(ends), err) + if err == nil && len(ends) > 0 { + src.lineTable = ends + } + } + + i.addrToSource.Add(addr, src) + return src, nil +} + +// getSFI reads and caches needed V8 SharedFunctionInfo object data. +func (i *v8Instance) getSFI(taggedPtr libpf.Address) (*v8SFI, error) { + if value, ok := i.addrToSFI.Get(taggedPtr); ok { + return value, nil + } + + vms := &i.d.vmStructs + addr, err := i.getTypedObject(taggedPtr, vms.Type.SharedFunctionInfo) + if err != nil { + return nil, fmt.Errorf("failed to read SFI: %w", err) + } + + // Read the function name + nosAddr, nosType, err := i.readObjectPtr(addr + + libpf.Address(vms.SharedFunctionInfo.NameOrScopeInfo)) + if err != nil { + return nil, err + } + sfi := &v8SFI{ + source: unknownSource, + bytecodeDeltaSeen: make(libpf.Set[uint32]), + } + switch { + case nosType == vms.Type.ScopeInfo: + sfi.funcName, sfi.funcStartPos, sfi.funcEndPos, err = i.analyzeScopeInfo(nosAddr) + case nosType < vms.Fixed.FirstNonstringType: + sfi.funcName, err = i.getString(nosAddr, nosType) + } + if err != nil { + sfi.funcName = fmt.Sprintf("<%s>", err) + } + if sfi.funcName == "" { + sfi.funcName = "" + } + + // Function data + // nolint:lll + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/shared-function-info.cc#76 + fdAddr, fdType, _ := i.readObjectPtr(addr + libpf.Address(vms.SharedFunctionInfo.FunctionData)) + switch fdType { + case vms.Type.BaselineData: + fdAddr, fdType, _ = i.readObjectPtr(fdAddr + libpf.Address(vms.BaselineData.Data)) + case vms.Type.Code: + // DeoptimizationData is really Bytecode for Baseline code + fdAddr, fdType, _ = i.readObjectPtr(fdAddr + libpf.Address(vms.Code.DeoptimizationData)) + } + if fdType == vms.Type.BytecodeArray { + length := decodeSMI(i.rm.Uint64(fdAddr + libpf.Address(vms.FixedArrayBase.Length))) + sfi.bytecodeLength = length + if length > 0 && length < 512*1024 && vms.BytecodeArray.Data != 0 { + log.Debugf("Bytecode available, %d bytes", length) + sfi.bytecode = make([]byte, length) + err = i.rm.Read(fdAddr+libpf.Address(vms.BytecodeArray.Data), sfi.bytecode) + if err != nil { + return nil, err + } + } else { + log.Debugf("Bytecode, %d bytes, not available", length) + } + sfi.bytecodePositionTable, err = i.readFixedTablePtr( + fdAddr+libpf.Address(vms.BytecodeArray.SourcePositionTable), + vms.Type.ByteArray, 1, 0) + log.Debugf("Bytecode positions: %d bytes: %v", len(sfi.bytecodePositionTable), err) + } + + // Script + sodiAddr, sodiType, _ := i.readObjectPtr(addr + + libpf.Address(vms.SharedFunctionInfo.ScriptOrDebugInfo)) + if sodiType == vms.Type.Script { + sfi.source, _ = i.getSource(sodiAddr) + if sfi.funcStartPos != sfi.funcEndPos { + sfi.funcStartLine = mapPositionToLine(sfi.source.lineTable, + int32(sfi.funcStartPos)) + } + } + + log.Debugf("SFI %#x: name: %v, start/end: %v/%v, file/line: %v:%v, #sourceLines: %d", + taggedPtr, sfi.funcName, sfi.funcStartPos, sfi.funcEndPos, + sfi.source.fileName, sfi.funcStartLine, len(sfi.source.lineTable)) + + // Synthesize function ID hash + h := fnv.New128a() + _, _ = h.Write([]byte(sfi.source.fileName)) + _, _ = h.Write([]byte(sfi.funcName)) + _, _ = h.Write(sfi.bytecode) + _, _ = h.Write(sfi.bytecodePositionTable) + sfi.funcID, err = libpf.FileIDFromBytes(h.Sum(nil)) + if err != nil { + return nil, fmt.Errorf("failed to create a function object ID: %v", err) + } + + i.addrToSFI.Add(taggedPtr, sfi) + return sfi, nil +} + +// readCode reads and caches needed V8 Code object data. +func (i *v8Instance) readCode(taggedPtr libpf.Address, cookie uint32, sfi *v8SFI) (*v8Code, error) { + vms := &i.d.vmStructs + + codeAddr, err := i.getTypedObject(taggedPtr, vms.Type.Code) + if err != nil { + return nil, fmt.Errorf("code pointer read: %v", err) + } + + // Read the class Code contained data up to the largest offset we need + // This follows the layout described in vms.Code: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#467 + codeSize := vms.Code.Flags + 4 + code := make([]byte, codeSize) + err = i.rm.Read(codeAddr, code) + if err != nil { + return nil, fmt.Errorf("code object read: %v", err) + } + + // Code Kind + codeFlags := npsr.Uint32(code, uint(vms.Code.Flags)) + codeKind := uint8((codeFlags & vms.CodeKind.FieldMask) >> vms.CodeKind.FieldShift) + + // Read in full source position tables + sourcePositionPtr := npsr.Ptr(code, uint(vms.Code.SourcePositionTable)) + sourcePositionPtr, err = i.getTypedObject(sourcePositionPtr, vms.Type.ByteArray) + if err != nil { + return nil, fmt.Errorf("code source position pointer read: %v", err) + } + codePositionTable, err := i.readFixedTable(sourcePositionPtr, 1, 0) + if err != nil { + return nil, fmt.Errorf("code source position table read: %v", err) + } + + // Baseline Code does not have deoptimization data + if codeKind == vms.CodeKind.Baseline { + if sfi == nil { + return nil, fmt.Errorf("baseline function without SFI") + } + + log.Debugf("Baseline Code %#x read: posSize: %v, cookie: %x", + codeAddr, len(codePositionTable), cookie) + + v8code := &v8Code{ + codeDeltaToPosition: make(map[uint32]sourcePosition), + sfi: sfi, + isBaseline: true, + codePositionTable: codePositionTable, + cookie: cookie, + } + i.addrToCode.Add(taggedPtr, v8code) + return v8code, nil + } + + // Read the deoptimization data + deoptimizationDataPtr := npsr.Ptr(code, uint(vms.Code.DeoptimizationData)) + deoptimizationDataPtr, err = i.getTypedObject(deoptimizationDataPtr, vms.Type.FixedArray) + if err != nil { + return nil, fmt.Errorf("deoptimization data pointer read: %v", err) + } + numItemsNeeded := uint32(vms.DeoptimizationDataIndex.InliningPositions + 1) + deoptimizationData, err := i.readFixedTable(deoptimizationDataPtr, pointerSize, numItemsNeeded) + if err != nil { + return nil, fmt.Errorf("deoptimization pointer read: %v", err) + } + if len(deoptimizationData) < pointerSize*int(numItemsNeeded) { + return nil, fmt.Errorf("DeoptimizationData array length too small: %d", + len(deoptimizationData)) + } + + if sfi == nil { + // Read the Code's SFI + sfiPtr := npsr.Ptr(deoptimizationData, + uint(vms.DeoptimizationDataIndex.SharedFunctionInfo*pointerSize)) + sfi, err = i.getSFI(sfiPtr) + if err != nil { + return nil, fmt.Errorf("getSFI: %w", err) + } + } + + // Read the Code's deoptimization data + numSFI := decodeSMI(npsr.Uint64(deoptimizationData, + uint(vms.DeoptimizationDataIndex.InlinedFunctionCount*pointerSize))) + + var inliningSFIs, inliningPositions []byte + if numSFI > 0 { + // The first numSFI entries of literal array are the pointers for + // inlined function's SFI structures + expectedTag := vms.Type.FixedArray + if vms.DeoptimizationLiteralArray.WeakFixedArray { + expectedTag = vms.Type.WeakFixedArray + } + literalArrayPtr := npsr.Ptr(deoptimizationData, + uint(vms.DeoptimizationDataIndex.LiteralArray*pointerSize)) + literalArrayPtr, err = i.getTypedObject(literalArrayPtr, expectedTag) + if err != nil { + return nil, fmt.Errorf("literal array pointer read: %v", err) + } + + inliningSFIs, err = i.readFixedTable(literalArrayPtr, pointerSize, numSFI) + if err != nil { + return nil, fmt.Errorf("literal array data read: %v", err) + } + + // Read the complete inlining positions structure + inliningPositionsPtr := npsr.Ptr(deoptimizationData, + uint(vms.DeoptimizationDataIndex.InliningPositions*pointerSize)) + inliningPositionsPtr, err = i.getTypedObject(inliningPositionsPtr, vms.Type.ByteArray) + if err != nil { + return nil, fmt.Errorf("inlining position pointer read: %v", err) + } + inliningPositions, err = i.readFixedTable(inliningPositionsPtr, 1, 0) + if err != nil { + return nil, fmt.Errorf("inlining position data read: %v", err) + } + } + + log.Debugf("Code %#x read: posSize: %v, sfiSize: %v, inlineSize: %v cookie: %x", + codeAddr, len(codePositionTable), len(inliningSFIs), + len(inliningPositions), cookie) + + v8code := &v8Code{ + codeDeltaToPosition: make(map[uint32]sourcePosition), + sfi: sfi, + codePositionTable: codePositionTable, + inliningSFIs: inliningSFIs, + inliningPositions: inliningPositions, + cookie: cookie, + } + i.addrToCode.Add(taggedPtr, v8code) + return v8code, nil +} + +// getCode reads and caches needed V8 Code object data from a Code pointer. +func (i *v8Instance) getCode(taggedPtr libpf.Address, cookie uint32) (*v8Code, error) { + if code, ok := i.addrToCode.Get(taggedPtr); ok { + if code.cookie == cookie { + return code, nil + } + i.addrToCode.Remove(taggedPtr) + } + return i.readCode(taggedPtr, cookie, nil) +} + +// getCodeFromJSFunction reads and caches needed V8 Code object data from a JSFunction pointer. +func (i *v8Instance) getCodeFromJSFunc(taggedPtr libpf.Address, cookie uint32) (*v8Code, error) { + if code, ok := i.addrToCode.Get(taggedPtr); ok { + if code.cookie == cookie { + return code, nil + } + i.addrToCode.Remove(taggedPtr) + } + + vms := &i.d.vmStructs + jsfuncAddr := taggedPtr &^ HeapObjectTagMask + + // Read needed JSFunction object data + jsfuncSize := max(vms.JSFunction.SharedFunctionInfo, vms.JSFunction.Code) + pointerSize + jsfunc := make([]byte, jsfuncSize) + err := i.rm.Read(jsfuncAddr, jsfunc) + if err != nil { + return nil, fmt.Errorf("jsfunc object read: %v", err) + } + + sfi, err := i.getSFI(npsr.Ptr(jsfunc, uint(vms.JSFunction.SharedFunctionInfo))) + if err != nil { + return nil, fmt.Errorf("getSFI: %w", err) + } + + // Chase and read the Code object + codeTaggedPtr := npsr.Ptr(jsfunc, uint(vms.JSFunction.Code)) + return i.readCode(codeTaggedPtr, cookie, sfi) +} + +// decodeUVLQ reads and decodes one unsigned Variable Length Quantity +// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/base/vlq.h#104 +func decodeUVLQ(r io.ByteReader) (uint64, error) { + // Base-128 or variable-length decoding. + // MSB indicates if there's more bytes to be read or not. + decoded := uint64(0) + for shift := 0; true; shift += 7 { + cur, err := r.ReadByte() + if err != nil { + return 0, err + } + decoded |= uint64(cur&0x7f) << shift + if cur&0x80 == 0 { + break + } + } + return decoded, nil +} + +// decodeVLQ reads and decodes one signed Variable Length Quantity +// nolint:lll +// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/base/vlq.h#110 +// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position-table.cc#90 +func decodeVLQ(r io.ByteReader) (int64, error) { + decoded, err := decodeUVLQ(r) + if err != nil { + return 0, err + } + // Sign decoding: LSB determines sign + return int64((decoded >> 1) ^ -(decoded & 1)), nil +} + +// nolint:lll +// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position.h#145 +// +// sourcePositition a native V8 datatype defined as a bitfield with following bits: +// +// is_external 1 bit +// IF is_external { +// external_line 20 bits +// external_file 10 bits +// } ELSE { +// script_offset 30 bits +// } +// inlining_id 16 bits +type sourcePosition uint64 + +func (pos sourcePosition) isExternal() bool { + return pos&1 == 1 +} + +func (pos sourcePosition) inliningID() uint16 { + return uint16(pos >> 31) +} + +func (pos sourcePosition) scriptOffset() int32 { + if pos.isExternal() { + return 0 + } + return int32((pos >> 1) & ((1 << 30) - 1)) +} + +// decodePosition walks the given position table to find the value matching 'delta'. +// This maps a byte/native code 'delta' to a source code position (byte offset). +func decodePosition(table []byte, delta uint64) sourcePosition { + // Logically the position table consists of lines with number pairs: + // (code position, source position) and is sorted monotonically by the + // code position. The actual stored zigzag value for both entries the + // delta from previous entry. + + r := bytes.NewReader(table) + // nolint:lll + // Initialize implicit base values: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position-table.h#26 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/common/globals.h#480 + codePos := int64(-1) + sourcePos := int64(0) + for { + codeDelta, err := decodeVLQ(r) + if err != nil { + if err == io.EOF { + return sourcePosition(sourcePos) + } + return 1 + } + if codeDelta >= 0 { + codePos += codeDelta + } else { + codePos -= codeDelta + 1 + } + + // Stop when we find a code position greater than the one we are looking for + if codePos > int64(delta) { + return sourcePosition(sourcePos) + } + + sourceDelta, err := decodeVLQ(r) + if err != nil { + return 1 + } + sourcePos += sourceDelta + } +} + +// 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 { + if len(lineEnds) == 0 || pos < 0 { + return 0 + } + // Use binary search to locate the line number + index := sort.Search(len(lineEnds), func(ndx int) bool { + return lineEnds[ndx] >= uint32(pos) + }) + return libpf.SourceLineno(index + 1) +} + +// scriptOffsetToLine maps a sourcePosition to a line number in the corresponding source +func (sfi *v8SFI) scriptOffsetToLine(position sourcePosition) libpf.SourceLineno { + scriptOffset := position.scriptOffset() + // The scriptOffset is offset by one, to make kNoSourcePosition zero. + // nolint:lll + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position.h#93 + if scriptOffset == 0 { + return sfi.funcStartLine + } + return mapPositionToLine(sfi.source.lineTable, scriptOffset-1) +} + +// symbolize symbolizes the raw frame data +func (i *v8Instance) symbolize(symbolizer interpreter.Symbolizer, sfi *v8SFI, + pos libpf.AddressOrLineno, lineNo libpf.SourceLineno) { + funcOffset := uint32(0) + if lineNo > sfi.funcStartLine { + funcOffset = uint32(lineNo - sfi.funcStartLine) + } + + symbolizer.FrameMetadata( + sfi.funcID, pos, lineNo, funcOffset, + sfi.funcName, sfi.source.fileName) + + log.Debugf("[%x] %v+%v at %v:%v", + sfi.funcID, + sfi.funcName, funcOffset, + sfi.source.fileName, lineNo) +} + +// generateNativeFrame and conditionally symbolizes a native frame. +func (i *v8Instance) generateNativeFrame(symbolizer interpreter.Symbolizer, + sourcePos sourcePosition, sfi *v8SFI, seen bool, + trace *libpf.Trace) { + if sourcePos.isExternal() { + // It is not easily possible to extract the external file's name. + // Just generate a place holder stub frame for external reference. + if i.d.externalStubID == 0 { + i.d.externalStubID = i.calculateAndSymbolizeStubID( + symbolizer, "") + } + insertFrame(trace, v8StubsFileID, i.d.externalStubID) + return + } + + lineNo := sfi.scriptOffsetToLine(sourcePos) + addressOrLineno := libpf.AddressOrLineno(lineNo) + nativeCodeBaseAddress + insertFrame(trace, sfi.funcID, addressOrLineno) + if !seen { + i.symbolize(symbolizer, sfi, addressOrLineno, lineNo) + } +} + +// symbolizeBytecode symbolizes and records to a trace a Bytecode based frame. +func (i *v8Instance) symbolizeBytecode(symbolizer interpreter.Symbolizer, 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) + 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, + delta uint64, trace *libpf.Trace) error { + vms := &i.d.vmStructs + sfi, err := i.getSFI(pointer) + if err != nil { + return fmt.Errorf("getSFI: %w", err) + } + + // Adjust the bytecode pointer as needed + // nolint:lll + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frames.cc#1793 + bytecodeDelta := int64(delta & C.V8_LINE_DELTA_MASK) + bytecodeDelta -= int64(vms.BytecodeArray.Data) - HeapObjectTag + if bytecodeDelta < 0 { + // Should not be happening + bytecodeDelta = 0 + } else if bytecodeDelta >= int64(sfi.bytecodeLength) { + // Invalid value + bytecodeDelta = nativeCodeBaseAddress - 1 + } + return i.symbolizeBytecode(symbolizer, sfi, uint64(bytecodeDelta), trace) +} + +// getBytecodeLength decodes the length at the start of bytecode array +func (d *v8Data) readBytecodeLength(r *bytes.Reader) int { + // Bytecode has one optional scaling prefix opcode, followed with the meaningful opcode. + // nolint:lll + // The list of these opcodes is at: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#46 + opcode, err := r.ReadByte() + if err != nil { + return 0 + } + + // The scaling opcodes have not changed since V8 6.8.141. So hard code them here. + // nolint:lll + // Map scaling opcodes to their offset in the decoding table: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#612 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#892 + scaleOffset := uint(0) + switch opcode { + case 0x00, 0x02: // Wide, DebugBreakWide + scaleOffset = uint(d.bytecodeCount) + case 0x01, 0x03: // ExtraWide, DebugBreakExtraWide + scaleOffset = 2 * uint(d.bytecodeCount) + } + + prefixSize := 0 + if scaleOffset != 0 { + prefixSize = 1 + opcode, err = r.ReadByte() + if err != nil { + return -1 + } + } + + // Get the length from kBytecodeSizes + // nolint:lll + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#893 + if opcode <= 0x03 && opcode > d.bytecodeCount { + // Invalid opcode + return -1 + } + opcodeLength := int(d.bytecodeSizes[scaleOffset+uint(opcode)]) + if _, err := r.Seek(int64(opcodeLength)-1, io.SeekCurrent); err != nil { + return -1 + } + + return prefixSize + opcodeLength +} + +// mapBaselineCodeOffsetToBytecode decodes a Baseline PC offset into Bytecode offset +func (i *v8Instance) mapBaselineCodeOffsetToBytecode(code *v8Code, pcDelta uint32) uint32 { + d := i.d + if d.bytecodeSizes == nil || code.sfi.bytecode == nil || code.codePositionTable == nil { + // Use the zero offset to report function start if needed data is not available. + return 0 + } + + // nolint:lll + // The algorithm to do the mapping: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/baseline/bytecode-offset-iterator.h#45 + // The baseline position table is a series of unsigned VLQs: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/baseline/bytecode-offset-iterator.h#78 + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/base/vlq.h#104 + pcReader := bytes.NewReader(code.codePositionTable) + pcOffset := uint64(0) + bytecodeReader := bytes.NewReader(code.sfi.bytecode) + bytecodeOffset := uint32(0) + + for { + pcLen, err := decodeUVLQ(pcReader) + if err != nil { + return 0 + } + pcOffset += pcLen + if pcOffset > uint64(pcDelta) { + return bytecodeOffset + } + + bytecodeLen := d.readBytecodeLength(bytecodeReader) + if bytecodeLen <= 0 { + // Invalid opcode, or end of bytecode + return 0 + } + bytecodeOffset += uint32(bytecodeLen) + } +} + +// symbolizeBaselineCode symbolizes and records to a trace a Baseline Code based frame. +func (i *v8Instance) symbolizeBaselineCode(symbolizer interpreter.Symbolizer, 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 + insertFrame(trace, code.sfi.funcID, libpf.AddressOrLineno(bytecodeDelta)) + return nil + } + + // 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) +} + +// 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 { + var err error + sfi := code.sfi + delta &= C.V8_LINE_DELTA_MASK + + // This is a native PC delta and points to the instruction after + // the call function. Adjust to get the CALL instruction. + if len(trace.FrameTypes) > 0 && delta > 0 { + delta-- + } + + if code.isBaseline { + return i.symbolizeBaselineCode(symbolizer, code, uint32(delta), trace) + } + + // Memoize the delta to position mapping to improve speed. We can't just + // fully skip inlining handling as it may expand this frame to multiple ones. + // However, we can skip symbolization of all frames if this delta was seen. + sourcePos, deltaSeen := code.codeDeltaToPosition[uint32(delta)] + if !deltaSeen { + sourcePos = decodePosition(code.codePositionTable, delta) + code.codeDeltaToPosition[uint32(delta)] = sourcePos + } + + for sourcePos.inliningID() != 0 { + // struct SourcePosition { + // uint64_t value_; + // }; + // struct InliningPosition { + // SourcePosition position = SourcePosition::Unknown(); + // int inlined_function_id; + // }; + + sizeofInliningPosition := uint(16) + inliningID := sourcePos.inliningID() + itemOff := uint(inliningID-1) * sizeofInliningPosition + funcID := npsr.Int32(code.inliningPositions, itemOff+8) + inlinedSFI := sfi + if funcID >= 0 { + sfiPtr := npsr.Ptr(code.inliningSFIs, uint(funcID)*pointerSize) + inlinedSFI, err = i.getSFI(sfiPtr) + if err != nil { + return fmt.Errorf("failed to get inlined SFI: %w", err) + } + } + i.generateNativeFrame(symbolizer, sourcePos, inlinedSFI, deltaSeen, trace) + + sourcePos = sourcePosition(npsr.Uint64(code.inliningPositions, itemOff)) + if sourcePos.inliningID() > inliningID { + // This should not happen. The inliningIDs seen should be + // monotonically decreasing. + return fmt.Errorf("inlining ID is not monotonically decreasing (%v <= %v)", + sourcePos.inliningID(), inliningID) + } + } + i.generateNativeFrame(symbolizer, sourcePos, sfi, deltaSeen, trace) + + return nil +} + +func (i *v8Instance) Symbolize(symbolReporter reporter.SymbolReporter, + frame *host.Frame, trace *libpf.Trace) error { + if !frame.Type.IsInterpType(libpf.V8) { + return interpreter.ErrMismatchInterpreterType + } + + sfCounter := successfailurecounter.New(&i.successCount, &i.failCount) + defer sfCounter.DefaultToFailure() + + pointerAndType := libpf.Address(frame.File) + deltaOrMarker := uint64(frame.Lineno) + frameType := pointerAndType & C.V8_FILE_TYPE_MASK + pointer := pointerAndType&^C.V8_FILE_TYPE_MASK | HeapObjectTag + + var err error + switch frameType { + case C.V8_FILE_TYPE_MARKER: + // This is a stub V8 frame, with deltaOrMarker containing the marker. + // Convert the V8 build specific marker ID to a static ID and symbolize + // that if needed. + err = i.symbolizeMarkerFrame(symbolReporter, deltaOrMarker, trace) + case C.V8_FILE_TYPE_BYTECODE, C.V8_FILE_TYPE_NATIVE_SFI: + err = i.symbolizeSFI(symbolReporter, pointer, deltaOrMarker, trace) + case C.V8_FILE_TYPE_NATIVE_CODE, C.V8_FILE_TYPE_NATIVE_JSFUNC: + var code *v8Code + codeCookie := uint32(deltaOrMarker & C.V8_LINE_COOKIE_MASK >> C.V8_LINE_COOKIE_SHIFT) + if frameType == C.V8_FILE_TYPE_NATIVE_CODE { + code, err = i.getCode(pointer, codeCookie) + } else { + code, err = i.getCodeFromJSFunc(pointer, codeCookie) + } + if err == nil { + err = i.symbolizeCode(symbolReporter, code, deltaOrMarker, trace) + } + default: + err = fmt.Errorf("unsupported frame type %#x", frameType) + } + if err != nil { + // TODO: emit error frame + return err + } + + sfCounter.ReportSuccess() + return nil +} + +func (d *v8Data) String() string { + ver := d.version + return fmt.Sprintf("V8 %d.%d.%d", (ver>>24)&0xff, (ver>>16)&0xff, ver&0xffff) +} + +// mapFramePointerOffset converts the frame pointer offset in bytes to eBPF used +// word offset relative to the number of slots read +func mapFramePointerOffset(relBytes uint8) C.u8 { + slotOffset := int(C.V8_FP_CONTEXT_SIZE) + int(int8(relBytes)) + if slotOffset < 0 || slotOffset > C.V8_FP_CONTEXT_SIZE-pointerSize { + return C.V8_FP_CONTEXT_SIZE + } + return C.u8(slotOffset) +} + +func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Address, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + vms := &d.vmStructs + data := C.V8ProcInfo{ + version: C.uint(d.version), + + fp_marker: mapFramePointerOffset(vms.FramePointer.Context), + fp_function: mapFramePointerOffset(vms.FramePointer.Function), + fp_bytecode_offset: mapFramePointerOffset(vms.FramePointer.BytecodeOffset), + + type_JSFunction_first: C.u16(vms.Fixed.FirstJSFunctionType), + type_JSFunction_last: C.u16(vms.Fixed.LastJSFunctionType), + type_Code: C.u16(vms.Type.Code), + type_SharedFunctionInfo: C.u16(vms.Type.SharedFunctionInfo), + + off_HeapObject_map: C.u8(vms.HeapObject.Map), + off_Map_instancetype: C.u8(vms.Map.InstanceType), + off_JSFunction_code: C.u8(vms.JSFunction.Code), + off_JSFunction_shared: C.u8(vms.JSFunction.SharedFunctionInfo), + + off_Code_instruction_start: C.u8(vms.Code.InstructionStart), + off_Code_instruction_size: C.u8(vms.Code.InstructionSize), + off_Code_flags: C.u8(vms.Code.Flags), + + codekind_shift: C.u8(vms.CodeKind.FieldShift), + codekind_mask: C.u8(vms.CodeKind.FieldMask), + codekind_baseline: C.u8(vms.CodeKind.Baseline), + } + if err := ebpf.UpdateProcData(libpf.V8, pid, unsafe.Pointer(&data)); err != nil { + return nil, err + } + + addrToString, err := freelru.New[libpf.Address, string](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + addrToCode, err := freelru.New[libpf.Address, *v8Code](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + addrToSFI, err := freelru.New[libpf.Address, *v8SFI](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + addrToSource, err := freelru.New[libpf.Address, *v8Source](lruSourceFileCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + addrToType, err := freelru.New[libpf.Address, uint16](lruMapTypeCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + return &v8Instance{ + d: d, + rm: rm, + mappings: make(map[process.Mapping]uint32), + prefixes: make(map[lpm.Prefix]uint32), + addrToString: addrToString, + addrToCode: addrToCode, + addrToSFI: addrToSFI, + addrToSource: addrToSource, + addrToType: addrToType, + }, nil +} + +func (d *v8Data) readIntrospectionData(ef *pfelf.File, syms libpf.SymbolFinder) error { + // Read the variables from the pfelf.File so we avoid failures if the process + // exists during extraction of the introspection data. + rm := ef.GetRemoteMemory() + + // Enumerate all symbols we are interested in + vms := &d.vmStructs + vmVal := reflect.ValueOf(vms).Elem() + vmType := reflect.TypeOf(vms).Elem() + for i := 0; i < vmVal.NumField(); i++ { + classVal := vmVal.Field(i) + classType := vmType.Field(i) + className := classType.Name + prefix := "v8dbg_" + if nameTag, ok := classType.Tag.Lookup("name"); ok { + className = nameTag + if nameTag != "" { + prefix += className + "_" + } + } else { + prefix += "class_" + className + "__" + } + + for j := 0; j < classVal.NumField(); j++ { + memberType := classVal.Type().Field(j) + memberVal := classVal.Field(j) + + memberName := memberType.Name + if nameTag, ok := memberType.Tag.Lookup("name"); ok { + memberName = nameTag + } + + for _, n := range strings.Split(memberName, ",") { + s := prefix + n + if memberVal.Kind() == reflect.Bool { + s = "v8dbg_parent_" + className + "__" + memberName + } + addr, err := syms.LookupSymbolAddress(libpf.SymbolName(s)) + if err != nil { + log.Debugf("V8: %s = not found", s) + if classType.Name == "FrameType" { + memberVal.SetUint(^uint64(0)) + } + continue + } + if memberVal.Kind() == reflect.Bool { + log.Debugf("V8: %s exists", s) + memberVal.SetBool(true) + } else { + val := rm.Uint32(libpf.Address(addr)) + log.Debugf("V8: %s = %#x", s, val) + memberVal.SetUint(uint64(val)) + } + break + } + } + } + + // Add some defaults when needed + if vms.FramePointer.BytecodeArray == 0 { + // Not available before V8 9.5.2 + if d.version >= v8Ver(8, 7, 198) { + vms.FramePointer.BytecodeArray = vms.FramePointer.Function - 2*pointerSize + } else { + vms.FramePointer.BytecodeArray = vms.FramePointer.Function - 1*pointerSize + } + } + if vms.FramePointer.BytecodeOffset == 0 { + // Not available before V8 9.5.2 + vms.FramePointer.BytecodeOffset = vms.FramePointer.BytecodeArray - pointerSize + } + if vms.Fixed.FirstJSFunctionType == 0 { + // nolint:lll + // The V8 InstaceTypes tags are defined at: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/instance-type.h#124 + // which in turn is generated from the data at: + // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/v8heapconst.py#175 + // Since V8 9.0.14 the JSFunction is no longer a final class, but has several + // classes inheriting form it. The only way to check for the inheritance is to + // know which InstaceType tags belong to the range. + numJSFuncTypes := uint16(1) + switch { + case d.version >= v8Ver(9, 6, 138): + // Class constructor special case + // nolint:lll + // https://chromium.googlesource.com/v8/v8.git/+/1cd7a5822374a49ab6767185e69119d0d3076840 + numJSFuncTypes = 15 + case d.version >= v8Ver(9, 0, 14): + // Several constructor special cases added + // nolint:lll + // https://chromium.googlesource.com/v8/v8.git/+/624030e975cb4384f877b65070b4e650a6acb1ef + numJSFuncTypes = 14 + } + vms.Fixed.FirstJSFunctionType = vms.Type.JSFunction + vms.Fixed.LastJSFunctionType = vms.Fixed.FirstJSFunctionType + numJSFuncTypes - 1 + } + if vms.JSFunction.Code == 0 { + if d.version >= v8Ver(11, 7, 368) { + vms.JSFunction.Code = vms.JSFunction.SharedFunctionInfo - pointerSize + } else { + // At least back to V8 8.4 + vms.JSFunction.Code = vms.JSFunction.SharedFunctionInfo + 3*pointerSize + } + } + if vms.Code.InstructionSize != 0 { + if vms.Code.SourcePositionTable == 0 { + // At least back to V8 8.4 + vms.Code.SourcePositionTable = vms.Code.InstructionSize - 2*pointerSize + } + if vms.Code.Flags == 0 { + // Back to V8 8.8.172 + vms.Code.Flags = vms.Code.InstructionSize + 2*4 // 2*sizeof(int) + } + } else if vms.Code.SourcePositionTable != 0 { + // Likely V8 11.x where the Code postmortem data was accidentally deleted + if vms.Code.DeoptimizationData == 0 { + vms.Code.DeoptimizationData = vms.Code.SourcePositionTable - pointerSize + } + if vms.Code.InstructionStart == 0 { + vms.Code.InstructionStart = vms.Code.SourcePositionTable + 2*pointerSize + } + if vms.Code.Flags == 0 { + vms.Code.Flags = vms.Code.InstructionStart + pointerSize + } + if vms.Code.InstructionSize == 0 { + vms.Code.InstructionSize = vms.Code.Flags + 4 + if d.version < v8Ver(11, 4, 59) { + // V8 starting 11.1.x Code has kBuiltinIdOffset and kKindSpecificFlagsOffset + // which changed again in 11.4.59 when these were removed in commit + // cb8be519f0add9b7 "[code] Merge kind_specific_flags with flags" + vms.Code.InstructionSize += 2 + 2 + } + } + } + + if vms.Code.DeoptimizationData == 0 && vms.Code.SourcePositionTable != 0 { + // Used unconditionally, pending patch for V8 to export this + // At least back to V8 7.2 + vms.Code.DeoptimizationData = vms.Code.SourcePositionTable - pointerSize + } + if vms.Script.Source == 0 { + // At least back to V8 8.4 + vms.Script.Source = vms.Script.Name - pointerSize + } + if vms.BytecodeArray.SourcePositionTable == 0 { + // Lost in V8 9.4 + vms.BytecodeArray.SourcePositionTable = vms.FixedArrayBase.Length + 3*pointerSize + } + if vms.BytecodeArray.Data == 0 { + // At least back to V8 8.4 (16 = 3*int32 + uint16) + vms.BytecodeArray.Data = vms.BytecodeArray.SourcePositionTable + pointerSize + 14 + } + if vms.DeoptimizationDataIndex.InlinedFunctionCount == 0 { + vms.DeoptimizationDataIndex.InlinedFunctionCount = 1 + } + if vms.DeoptimizationDataIndex.LiteralArray == 0 { + val := vms.DeoptimizationDataIndex.InlinedFunctionCount + 1 + vms.DeoptimizationDataIndex.LiteralArray = val + } + if vms.DeoptimizationDataIndex.SharedFunctionInfo == 0 { + vms.DeoptimizationDataIndex.SharedFunctionInfo = 6 + } + if vms.DeoptimizationDataIndex.InliningPositions == 0 { + val := vms.DeoptimizationDataIndex.SharedFunctionInfo + 1 + vms.DeoptimizationDataIndex.InliningPositions = val + } + if vms.CodeKind.Baseline == 0 { + if d.version >= v8Ver(9, 0, 240) { + // Back to V8 9.0.240, and metadata available after that + vms.CodeKind.FieldMask = 0xf + vms.CodeKind.FieldShift = 0 + vms.CodeKind.Baseline = 11 + } else { + // Leave mask and shift to zero, and set baseline to something + // so that the Baseline code is never triggered. + vms.CodeKind.Baseline = 0xff + } + } + if vms.BaselineData.Data == 0 && vms.CodeKind.FieldMask != 0 { + // Unfortunately no metadata currently. Has been static. + vms.BaselineData.Data = vms.HeapObject.Map + 2*pointerSize + } + + for i := 0; i < vmVal.NumField(); i++ { + classVal := vmVal.Field(i) + classType := vmType.Field(i) + for j := 0; j < classVal.NumField(); j++ { + memberType := classVal.Type().Field(j) + memberVal := classVal.Field(j) + log.Debugf("V8: %s::%s = %#x", classType.Name, memberType.Name, memberVal.Interface()) + if memberVal.Kind() == reflect.Bool || memberVal.Uint() != 0 { + continue + } + if _, ok := memberType.Tag.Lookup("zero"); !ok { + return fmt.Errorf("failed to get introspection data for %s.%s", + classType.Name, memberType.Name) + } + } + } + + return nil +} + +func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + if !v8Regex.MatchString(info.FileName()) { + return nil, nil + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + var vers [3]uint32 + for i, sym := range []string{"major", "minor", "build"} { + var addr libpf.SymbolValue + var raw [4]byte + // Resolve and read "v8::internal::Versions::XXXXXX_E" + sym = fmt.Sprintf("_ZN2v88internal7Version6%s_E", sym) + addr, err = ef.LookupSymbolAddress(libpf.SymbolName(sym)) + if err == nil { + _, err = ef.ReadVirtualMemory(raw[:], int64(addr)) + } + if err != nil { + return nil, fmt.Errorf("symbol '%s': %v", sym, err) + } + vers[i] = npsr.Uint32(raw[:], 0) + } + + version := vers[0]*0x1000000 + vers[1]*0x10000 + vers[2] + log.Debugf("V8 version %v.%v.%v", vers[0], vers[1], vers[2]) + if vers[0] > 0xff || vers[1] > 0xff || vers[2] > 0xffff || version < 0x080100 { + return nil, fmt.Errorf("version %v.%v.%v of V8 is not supported (minimum is 8.1.0)", + vers[0], vers[1], vers[2]) + } + + var syms libpf.SymbolFinder + syms, err = ef.ReadDynamicSymbols() + if err != nil { + // Dynamic section does not exists for core dumps. Use the pfelf as + // symbol finder then. + syms = ef + } + + d := &v8Data{ + version: version, + } + + addr, err := syms.LookupSymbolAddress("_ZN2v88internal8Snapshot19DefaultSnapshotBlobEv") + if err == nil { + // If there is a big stack delta soon after v8::internal::Snapshot::DefaultSnapshotBlob() + // assume it is the V8 snapshot data. + for _, gap := range info.Gaps() { + if gap.Start-uint64(addr) < 1024 { + d.snapshotRange = gap + log.Debugf("V8 JIT Area: %#v", d.snapshotRange) + break + } + } + } + + sym, err := syms.LookupSymbol("_ZN2v88internal11interpreter9Bytecodes14kBytecodeSizesE") + if err == nil && sym.Size%3 == 0 && sym.Size < 3*256 { + // Symbol v8::internal::interpreter::Bytecodes::kBytecodeSizes: + // static const uint8_t Bytecodes::kBytecodeSizes[3][kBytecodeCount]; + log.Debugf("V8: bytecode sizes at %x, length %d, %d opcodes", + sym.Address, sym.Size, sym.Size/3) + d.bytecodeSizes = make([]byte, sym.Size) + d.bytecodeCount = uint8(sym.Size / 3) + if _, err = ef.ReadVirtualMemory(d.bytecodeSizes, int64(sym.Address)); err != nil { + return nil, fmt.Errorf("unable to read bytecode sizes: %v", err) + } + for _, opcodeLength := range d.bytecodeSizes { + // Check for valid opcode size. Largest seen so far is 17 bytes. + if opcodeLength <= 0 || opcodeLength >= 32 { + log.Debugf("V8: invalid bytecode opcode size: %d", opcodeLength) + d.bytecodeSizes = nil + d.bytecodeCount = 0 + break + } + } + } + + // load introspection data + if err = d.readIntrospectionData(ef, syms); err != nil { + return nil, err + } + + vms := &d.vmStructs + if vms.Fixed.HeapObjectTag != HeapObjectTag || + vms.Fixed.HeapObjectTagMask != HeapObjectTagMask || + vms.Fixed.SmiTag != SmiTag || vms.Fixed.SmiTagMask != SmiTagMask || + vms.Fixed.SmiShiftSize != SmiValueShift-SmiTagShift { + return nil, fmt.Errorf("incompatible tagging scheme") + } + + if mapFramePointerOffset(vms.FramePointer.Context) >= C.V8_FP_CONTEXT_SIZE || + mapFramePointerOffset(vms.FramePointer.Function) >= C.V8_FP_CONTEXT_SIZE || + mapFramePointerOffset(vms.FramePointer.BytecodeOffset) >= C.V8_FP_CONTEXT_SIZE { + return nil, fmt.Errorf("incompatible framepointer offsets (%d/%d/%d)", + vms.FramePointer.Context, vms.FramePointer.Function, + vms.FramePointer.BytecodeOffset) + } + + if d.snapshotRange.Start != 0 { + if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindV8, info.FileID(), + []libpf.Range{d.snapshotRange}); err != nil { + return nil, err + } + } + + return d, nil +} diff --git a/interpreter/perl/perl.go b/interpreter/perl/perl.go new file mode 100644 index 00000000..98e26958 --- /dev/null +++ b/interpreter/perl/perl.go @@ -0,0 +1,850 @@ +/* + * 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 + +// Perl interpreter unwinder + +// Start by reading the 'perlguts illustrated' it is really helpful on explaining +// the Perl VM internal implementation: +// https://github.com/rurban/illguts/blob/master/illguts.pdf +// +// Additional recommended reading from the Perl manuals: +// https://perldoc.perl.org/perlguts#Dynamic-Scope-and-the-Context-Stack +// https://perldoc.perl.org/perlinterp#OP-TREES +// https://perldoc.perl.org/perlcall +// https://perldoc.perl.org/perlxs + +// It is said that reading Perl code makes your eyes bleed. +// I say reading Perl interpreter code with all the unsafe casting, +// and unions makes your brains bleed. -tt + +// Perl uses a SV (Scalar Value) as the base "variant" type for all Perl VM +// variables. It can be a one of various different types, but we are mostly +// interested about CV (Code Value), GV (Glob Value, aka. symbol name), and +// HV (Hash Value). Typically the only difference between e.g. SV and CV is +// only that the pointer is of different types, and casts between these structs +// are done if the type code shows the cast is ok. +// +// Much of the extra data is behind the "any" or "variant" pointer. Typically +// named XPVxV (where 'x' changes, so XPVCV for CV). Other types may have another +// additional pointer in the base 'SV' too, like the HV and GV. Bulk of the code +// is just following these pointers (ensuring right types). Please refer to the +// Perlguts illustrated for the relationships. +// +// See the perl_tracer.ebpf.c for more detailed unwinding explanation. +// The tracer will send the 'EGV' (effective GV, aka canonical symbol name) and +// the 'COP' for each frame. This code will stringify the EGV to a full qualified +// symbol name, and extract the source file/line from the COP. The EGV is null for +// the bottom frame which is the global file scope (not inside a function). +// +// Unfortunately, it is not possible to extract file/line where some function is +// defined. The observant may note that the 'struct gp' which is the symbol definition +// holds source file name and line number of its "first definition". But this refers +// either to the closing '}' of the sub definition, or the line of the object +// reference creation ... both of which are not useful for us. It really seems to +// not be possible to get a function's start line. + +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) + // https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L132-L166 + SVt_MASK uint32 = 0x1f + SVt_PVHV uint32 = 12 + + // Arbitrary string length limit to make sure we don't panic with out-of-memory + hekLenLimit = 0x10000 +) + +var ( + // regex for the interpreter executable + perlRegex = regexp.MustCompile(`^(?:.*/)?perl$`) + libperlRegex = regexp.MustCompile(`^(?:.*/)?libperl\.so[^/]*$`) + + // compiler check to make sure the needed interfaces are satisfied + _ interpreter.Data = &perlData{} + _ 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 +} + +func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + mainDSO := false + if !libperlRegex.MatchString(info.FileName()) { + mainDSO = true + if !perlRegex.MatchString(info.FileName()) { + return nil, nil + } + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + if mainDSO { + var needed []string + needed, err = ef.DynString(elf.DT_NEEDED) + if err != nil { + return nil, err + } + for _, n := range needed { + if libperlRegex.MatchString(n) { + // 'perl' linked with 'libperl'. The beef is in the library, + // so do not try to inspect the shim main binary. + return nil, nil + } + } + } + + // 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 +} diff --git a/interpreter/php/decode_amd64.c b/interpreter/php/decode_amd64.c new file mode 100644 index 00000000..c207d084 --- /dev/null +++ b/interpreter/php/decode_amd64.c @@ -0,0 +1,139 @@ +//go:build amd64 +#include +#include "decode_amd64.h" + + +// retrieveJITBufferPtr will decode instructions from the given code blob until +// an assignment has been made to rdi. This corresponds to loading +// the dasm_buf in preparation for a function call. +int retrieveJITBufferPtr(const uint8_t * const code, const size_t codesize, + const uint64_t rip_base, uint64_t * const buffer_ptr, + uint64_t * const size_ptr) { + ZydisDecoder decoder; + ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64); + ZydisDecodedInstruction instr; + ZyanUSize instruction_offset = 0; + + // These are to check that we've written to both pointers. + int written_to_buffer_ptr = 0; + int written_to_size_ptr = 0; + + while(ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + instruction_offset, + codesize - instruction_offset, &instr))) { + instruction_offset += instr.length; + if(instr.mnemonic == ZYDIS_MNEMONIC_CALL || instr.mnemonic == ZYDIS_MNEMONIC_JMP) { + // We should have returned by now, so return. + return EARLY_RETURN_ERROR; + } + + + // We only care about writing into rdi or rsi + if(instr.mnemonic != ZYDIS_MNEMONIC_MOV + || instr.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER + || !(instr.operands[0].reg.value == ZYDIS_REGISTER_RDI || + instr.operands[0].reg.value == ZYDIS_REGISTER_RSI)) { + continue; + } + + if(instr.operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY && + instr.operands[1].mem.disp.has_displacement && + instr.operands[1].mem.base == ZYDIS_REGISTER_RIP && + instr.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER) { + + if(instr.operands[0].reg.value == ZYDIS_REGISTER_RDI) { + *buffer_ptr = rip_base + instruction_offset + instr.operands[1].mem.disp.value; + written_to_buffer_ptr = 1; + } else if (instr.operands[0].reg.value == ZYDIS_REGISTER_RSI) { + *size_ptr = rip_base + instruction_offset + instr.operands[1].mem.disp.value; + written_to_size_ptr = 1; + } + } + + if(written_to_size_ptr && written_to_buffer_ptr) { + return NO_ERROR; + } + + } + return NOT_FOUND_ERROR; +} + +// retrieveExecuteExJumpLabelAddress will decode instructions from the given code blob until +// a jmp instruction is encountered. This corresponds to executing code in PHP's Hybrid VM, +// which allows us to recover accurate PC data for JIT code +int retrieveExecuteExJumpLabelAddress(const uint8_t * const code, const size_t codesize, + const uint64_t rip_base, uint64_t * const out) { + // The raison d'etre for this function is described in the php8 unwinding doc, + // in particular in the "disassembling execute_ex" section. + ZydisDecoder decoder; + ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64); + // Note: since we're recovering a theoretical return address we need to read "one ahead" + // so that we can return properly + ZydisDecodedInstruction instr; + ZyanUSize instruction_offset = 0; + + while(ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + instruction_offset, + codesize - instruction_offset, &instr))) { + instruction_offset += instr.length; + if(instr.mnemonic == ZYDIS_MNEMONIC_RET) { + // Unexpected early return indicating end of the function + // Getting here implies we've had an error. + return EARLY_RETURN_ERROR; + } + + // If the instruction is a jmp then we've found the right address. + if(instr.mnemonic == ZYDIS_MNEMONIC_JMP) { + // Read the next address. + if(ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + instruction_offset, + codesize - instruction_offset, &instr))) { + *out = instruction_offset + rip_base; + return NO_ERROR; + } else { + // If this fails it implies the buffer isn't big enough, or that + // the PHP code block is malformed, or our heuristic assumptions are wrong.. + // this is a larger error + return DECODING_ERROR; + } + } + } + + // Getting here implies we've had an error + return NOT_FOUND_ERROR; +} + + +// retrieveZendVMKind will decode instructions from the given code blob until an +// assignment to (e/r)ax has been made. This corresponds to loading an immediate in +// rax for the return from zend_vm_kind, which contains the VM Mode that we care about. +int retrieveZendVMKind(const uint8_t * const code, const size_t codesize, + uint64_t * const out) { + ZydisDecoder decoder; + ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64); + ZydisDecodedInstruction instr; + ZyanUSize instruction_offset = 0; + while(ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + instruction_offset, + codesize - instruction_offset, &instr))) { + instruction_offset += instr.length; + if(instr.mnemonic == ZYDIS_MNEMONIC_RET) { + // Unexpected early return indicating end of the function + // Getting here implies we've had an error. + return EARLY_RETURN_ERROR; + } + + // This corresponds to an instruction like this: + // mov eax, 0x... + // Note that since the immediate is likely small (e.g between 0-4) we check the + // destination register as both + // EAX and RAX to account for possible changes in codegen. + if(instr.mnemonic == ZYDIS_MNEMONIC_MOV && + instr.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && + instr.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && + ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, instr.operands[0].reg.value) == + ZYDIS_REGISTER_RAX) { + *out = instr.operands[1].imm.value.u; + return NO_ERROR; + } + } + + // We shouldn't get here, so if we do there's been an error. + return NOT_FOUND_ERROR; +} diff --git a/interpreter/php/decode_amd64.go b/interpreter/php/decode_amd64.go new file mode 100644 index 00000000..d04179e8 --- /dev/null +++ b/interpreter/php/decode_amd64.go @@ -0,0 +1,87 @@ +//go:build amd64 + +/* + * 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 ( + "fmt" + "unsafe" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +// #cgo CFLAGS: -g -Wall +// #cgo LDFLAGS: -lZydis +// #include "decode_amd64.h" +// #include "../../support/ebpf/types.h" +import "C" + +// phpDecodeErrorToString. This function converts an error code +// into a string that corresponds to it. +func phpDecodeErrorToString(errorCode int) string { + switch errorCode { + case C.NOT_FOUND_ERROR: + return "target not found" + case C.EARLY_RETURN_ERROR: + return "early return" + case C.DECODING_ERROR: + return "decoding error" + } + + return "unknown error code" +} + +// retrieveZendVMKindWrapper. This function reads the code blob and recovers +// the type of the PHP VM that is used by this process. +func retrieveZendVMKindWrapper(code []byte) (uint, error) { + var vmKind uint + err := int(C.retrieveZendVMKind((*C.uint8_t)(unsafe.Pointer(&code[0])), + C.size_t(len(code)), (*C.uint64_t)(unsafe.Pointer(&vmKind)))) + + if err == C.NO_ERROR { + return vmKind, nil + } + + return 0, fmt.Errorf("failed to decode zend_vm_kind: %s", phpDecodeErrorToString(err)) +} + +// retrieveExecuteExJumpLabelAddressWrapper. This function reads the code blob and returns +// the address of the return address for any JIT code called from execute_ex. Since all JIT +// code is ultimately called from execute_ex, this is the same as returning the return address +// for all JIT code. +func retrieveExecuteExJumpLabelAddressWrapper(code []byte, addrBase libpf.SymbolValue) ( + libpf.SymbolValue, error) { + var jumpAddress uint + err := int(C.retrieveExecuteExJumpLabelAddress((*C.uint8_t)(unsafe.Pointer(&code[0])), + C.size_t(len(code)), C.uint64_t(addrBase), (*C.uint64_t)(unsafe.Pointer(&jumpAddress)))) + + if err == C.NO_ERROR { + return libpf.SymbolValue(jumpAddress), nil + } + + return libpf.SymbolValueInvalid, + fmt.Errorf("failed to decode execute_ex: %s", phpDecodeErrorToString(err)) +} + +// 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) ( + dasmBuf libpf.SymbolValue, dasmSize libpf.SymbolValue, err error) { + var bufferAddress, sizeAddress uint + err2 := int(C.retrieveJITBufferPtr((*C.uint8_t)(unsafe.Pointer(&code[0])), + C.size_t(len(code)), C.uint64_t(addrBase), + (*C.uint64_t)(unsafe.Pointer(&bufferAddress)), + (*C.uint64_t)(unsafe.Pointer(&sizeAddress)))) + + if err2 == C.NO_ERROR { + return libpf.SymbolValue(bufferAddress), libpf.SymbolValue(sizeAddress), nil + } + + return libpf.SymbolValueInvalid, libpf.SymbolValueInvalid, + fmt.Errorf("failed to recover jit buffer: %s", phpDecodeErrorToString(err2)) +} diff --git a/interpreter/php/decode_amd64.h b/interpreter/php/decode_amd64.h new file mode 100644 index 00000000..ad5ad0e1 --- /dev/null +++ b/interpreter/php/decode_amd64.h @@ -0,0 +1,30 @@ +//go:build amd64 +#ifndef __INCLUDED_PHP_DECODE_X86_64__ +#define __INCLUDED_PHP_DECODE_X86_64__ +#include +#include + +// Note: to make it easier to convert C error codes into Go error strings +// we place an enum here that represents the set of allowed +// error codes. These represent the errors that could +// occur during the execution of each function. +enum x86PHPJITDecodingCodes { + // No error: happens when no error happens. + NO_ERROR = 0, + // Happens when we iterate over the whole blob + // without finding the target instruction + NOT_FOUND_ERROR = 1, + // Happens when we encounter a CALL/JMP before finding + // the target instruction + EARLY_RETURN_ERROR = 2, + // Happens when we fail to decode due to a small blob. + DECODING_ERROR = 3, +}; + +int retrieveExecuteExJumpLabelAddress(const uint8_t * const code, const size_t codesize, + const uint64_t rip_base, uint64_t * const out); +int retrieveZendVMKind(const uint8_t * const code, const size_t codesize, uint64_t * const out); +int retrieveJITBufferPtr(const uint8_t * const code, const size_t codesize, + const uint64_t rip_base, uint64_t * const buffer_ptr, + uint64_t * const size_ptr); +#endif diff --git a/interpreter/php/decode_arm64.go b/interpreter/php/decode_arm64.go new file mode 100644 index 00000000..a3ff897a --- /dev/null +++ b/interpreter/php/decode_arm64.go @@ -0,0 +1,202 @@ +//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. + * See the file "LICENSE" for details. + */ + +package php + +import ( + "fmt" + + "github.com/elastic/otel-profiling-agent/libpf" + ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + aa "golang.org/x/arch/arm64/arm64asm" +) + +// retrieveZendVMKindWrapper. This function reads the code blob and recovers +// the type of the PHP VM that is used by this process. +func retrieveZendVMKindWrapper(code []byte) (uint, error) { + // Here we need to decode assembly that looks like this: + // + // mov w0, ## constant + // ret + // + // This means all we need to do is look at movs into w0 + + // If the implementation isn't as described above, we should bail out. This could happen if the + // implementation of zend_vm_kind changes. We thus only allow two instructions to be processed. + maxOffs := min(len(code), 8) + + for offs := 0; offs < maxOffs; offs += 4 { + inst, err := aa.Decode(code[offs:]) + if err != nil { + return 0, fmt.Errorf("could not decode instruction at %d"+ + "in the given code blob", offs) + } + + // We only care about writes into w0 + dest, ok := ah.Xreg2num(inst.Args[0]) + if dest != 0 || !ok { + continue + } + + if inst.Op == aa.MOV { + val, ok := ah.DecodeImmediate(inst.Args[1]) + if !ok { + break + } + return uint(val), nil + } + } + + // If we haven't already returned then clearly we're in an error state. + return 0, fmt.Errorf("did not find a mov into w0 in the given code blob") +} + +// retrieveExecuteExJumpLabelAddressWrapper. This function reads the code blob and returns +// the address of the jump label for any JIT code called from execute_ex. Since all JIT +// code is ultimately called from execute_ex, this is the same as returning the return address +// for all JIT code. +func retrieveExecuteExJumpLabelAddressWrapper( + code []byte, addrBase libpf.SymbolValue) (libpf.SymbolValue, error) { + // Here we're looking for the first unrestricted jump in the execute_ex function + // The reasons for this are given in the php8 unwinding document, but essentially we + // heuristically found out that the php JIT code gets jumped into using GCC's "labels as + // values" feature + // + // In assembly terms this looks something like this: + // + // xxx - 4: ... + // xxx : br x0 + // xxx + 4: ... <---- This is the return address we care about. + // + // The heuristic is that the first br we encounter is the jump to the JIT code. This is because + // (in theory) the first unrestricted goto in the php interpreter loop is the jump to the + // handler for the particular zend_op. + // + // NOTE: we can't strengthen this check by also checking the register: PHP is __meant__ to store + // the handler pointer in x28, but this is routinely and systematically ignored by compilers. + + for offs := 0; offs < len(code); offs += 4 { + inst, err := aa.Decode(code[offs:]) + if err != nil { + return libpf.SymbolValueInvalid, + fmt.Errorf("could not decode the instruction at %d"+ + "in the given code blob", offs) + } + + // We only care about br instructions + if inst.Op == aa.BR && offs+4 < len(code) { + // The length check is enough to make sure this is + // a valid address. + return libpf.SymbolValue(offs+4) + addrBase, nil + } + } + 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 +// PHP (called "dasm_buf" in the PHP source). +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. + // + // The idea is still the same: we're looking for a ldr into x0 in preparation for a function + // call. Unfortunately, the Go disassembler makes it hard to do this sort of thing, so we need + // to track the offsets by hand so that we can recover the address. + // + // For example, this is a likely assembly snippet: + // + // adrp x1, 0xfffff55aa000 + // add x1, x1, #0xf8 + // ... + // ldr x0, [x1, #840] + // ldr x1, [x1, #850] + // + // Given that x0 depends on x1 we need to track the instructions + // that are issued and then produce the correct offset at the end. + // + // We also assume that the first BL we encounter is the one we care about. + // This is because the first call inside zend_jit_protect is a call to mprotect. + var regOffset [32]uint64 + + bufRetVal := libpf.SymbolValueInvalid + sizeRetVal := libpf.SymbolValueInvalid + + for offs := 0; offs < len(code); offs += 4 { + inst, err := aa.Decode(code[offs:]) + if err != nil { + return libpf.SymbolValueInvalid, + libpf.SymbolValueInvalid, + fmt.Errorf("could not decode instruction at %d"+ + "in the given code blob", offs) + } + + if inst.Op == aa.BL && bufRetVal != libpf.SymbolValueInvalid && + sizeRetVal != libpf.SymbolValueInvalid { + return bufRetVal, sizeRetVal, nil + } + + // We only care about writes into xn/wn registers. + dest, ok := ah.Xreg2num(inst.Args[0]) + if !ok { + continue + } + + switch inst.Op { + case aa.ADD: + a2, ok := ah.DecodeImmediate(inst.Args[2]) + if !ok { + break + } + + regOffset[dest] += a2 + case aa.ADRP: + // The offset here is a PCRel, so we + // can just recover it directly + // Note that GDB lies to you here: it will give you + // a different value in the ASM listing compared to the + // disassembler + a2, ok := ah.DecodeImmediate(inst.Args[1]) + if !ok { + break + } + + // The instruction specifies that this value needs to + // shifted about before being added to the PC. + pc := uint64(addrBase) + uint64(offs) + regOffset[dest] = ((pc + a2) >> 12) << 12 + case aa.LDR: + m, ok := inst.Args[1].(aa.MemImmediate) + if !ok { + break + } + + val, ok := ah.DecodeImmediate(m) + if !ok { + break + } + + src, ok := ah.Xreg2num(m.Base) + if !ok { + break + } + + // If we're writing to x0/x1 then these are potentially interesting + // values for us, so we'll recover them + switch dest { + case 0: + bufRetVal = libpf.SymbolValue(regOffset[src] + val) + case 1: + sizeRetVal = libpf.SymbolValue(regOffset[src] + val) + } + } + } + + return libpf.SymbolValueInvalid, libpf.SymbolValueInvalid, + fmt.Errorf("did not find a BL instruction in" + + "the given code blob") +} diff --git a/interpreter/php/php.go b/interpreter/php/php.go new file mode 100644 index 00000000..69f27050 --- /dev/null +++ b/interpreter/php/php.go @@ -0,0 +1,579 @@ +/* + * 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 ( + "bytes" + "fmt" + "hash/fnv" + "regexp" + "strconv" + "sync/atomic" + "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/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/support" + + log "github.com/sirupsen/logrus" +) + +// #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 ( + // maxPHPRODataSize is the maximum PHP RO Data segment size to scan + // (currently the largest seen is about 9M) + maxPHPRODataSize = 16 * 1024 * 1024 + + // unknownFunctionName is the name to be used when it cannot be read from the + // interpreter, or explicit function name does not exist (global code not in function) + unknownFunctionName = "" + + // evalCodeFunctionName is a placeholder name to show that code has been evaluated + // using eval in PHP. + evalCodeFunctionName = "" +) + +var ( + // regex for the interpreter executable + phpRegex = regexp.MustCompile(".*/php(-cgi|-fpm)?[0-9.]*$|^php(-cgi|-fpm)?[0-9.]*$") + versionMatch = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)`) + + // compiler check to make sure the needed interfaces are satisfied + _ interpreter.Data = &php7Data{} + _ interpreter.Instance = &php7Instance{} +) + +type php7Data struct { + version uint + + // egAddr is the `executor_globals` symbol value which is needed by the eBPF + // program to build php backtraces. + egAddr libpf.Address + + // rtAddr is the `return address` for the JIT code (in Hybrid mode). + // This is described in more detail in the PHP unwinding doc, + // but the short description is that PHP call stacks don't always + // store return addresses. + rtAddr libpf.Address + + // vmStructs reflects the PHP internal class names and the offsets of named field + // nolint:golint,stylecheck,revive + vmStructs struct { + // https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_globals.h#L135 + zend_executor_globals struct { + current_execute_data uint + } + // https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_compile.h#L503 + zend_execute_data struct { + opline, function uint + this_type_info uint + prev_execute_data uint + } + // https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_compile.h#L483 + zend_function struct { + common_type, common_funcname uint + op_array_filename, op_array_linestart uint + Sizeof uint + } + // https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_types.h#L235 + zend_string struct { + val libpf.Address + } + // https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_compile.h#L136 + zend_op struct { + lineno uint + } + } +} + +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 { + 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, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + addrToFunction, err := + freelru.New[libpf.Address, *phpFunction](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + vms := &d.vmStructs + data := C.PHPProcInfo{ + current_execute_data: C.u64(d.egAddr+bias) + + C.u64(vms.zend_executor_globals.current_execute_data), + jit_return_address: C.u64(d.rtAddr + bias), + zend_execute_data_function: C.u8(vms.zend_execute_data.function), + zend_execute_data_opline: C.u8(vms.zend_execute_data.opline), + zend_execute_data_prev_execute_data: C.u8(vms.zend_execute_data.prev_execute_data), + zend_execute_data_this_type_info: C.u8(vms.zend_execute_data.this_type_info), + zend_function_type: C.u8(vms.zend_function.common_type), + zend_op_lineno: C.u8(vms.zend_op.lineno), + } + if err := ebpf.UpdateProcData(libpf.PHP, pid, unsafe.Pointer(&data)); err != nil { + return nil, err + } + + instance := &php7Instance{ + d: d, + rm: rm, + addrToFunction: addrToFunction, + } + + // 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 { + instance.vmRTCount.Store(1) + } + + return instance, nil +} + +func VersionExtract(rodata string) (uint, error) { + matches := versionMatch.FindStringSubmatch(rodata) + if matches == nil { + return 0, fmt.Errorf("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 +} + +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") + } + + needle := []byte("X-Powered-By: PHP/") + for _, segment := range ef.ROData { + rodata, err := segment.Data(maxPHPRODataSize) + if err != nil { + return 0, err + } + idx := bytes.Index(rodata, needle) + if idx < 0 { + continue + } + + idx += len(needle) + zeroIdx := bytes.IndexByte(rodata[idx:], 0) + if zeroIdx < 0 { + continue + } + 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") +} + +func recoverExecuteExJumpLabelAddress(ef *pfelf.File) (libpf.SymbolValue, error) { + // This function recovers the return address for JIT'd PHP code by + // disassembling the execute_ex function. This is entirely heuristic and + // described in some detail in the PHP8 unwinding document in the "disassembling + // execute_ex" section. This is only useful for PHP8+ + + // Zend/zend_vm_execute.h: execute_ex(zend_execute_data *ex) is the main VM + // executor function, has been such at least since PHP7.0. This is guaranteed + // to be the vm executor function in PHP JIT'd code, since the JIT is (currently) + // inoperable with overridden execute_ex's + executeExAddr, err := ef.LookupSymbolAddress("execute_ex") + if err != nil { + return libpf.SymbolValueInvalid, + fmt.Errorf("could not find execute_ex: %w", err) + } + + // The address we care about varies from being 47 bytes in to about 107 bytes in, + // so we'll read 128 bytes. This might need to be adjusted up in future. + code := make([]byte, 128) + if _, err = ef.ReadVirtualMemory(code, int64(executeExAddr)); err != nil { + return libpf.SymbolValueInvalid, + fmt.Errorf("could not read from executeExAddr: %w", err) + } + + returnAddress, err := retrieveExecuteExJumpLabelAddressWrapper(code, executeExAddr) + if err != nil { + return libpf.SymbolValueInvalid, + fmt.Errorf("reading the return address from execute_ex failed (%w)", + err) + } + + return returnAddress, nil +} + +func determineVMKind(ef *pfelf.File) (uint, error) { + // This function recovers the PHP VM mode from the PHP binary + // This is a compile-time configuration option that configures + // how the PHP VM calls functions. This is only useful for PHP8+ + + // This is a publicly exposed function in PHP that returns the VM type + // This has been implemented in PHP since at least 7.2 + vmKindAddr, err := ef.LookupSymbolAddress("zend_vm_kind") + if err != nil { + return 0, fmt.Errorf("zend_vm_kind not found: %w", err) + } + + // We should only need around 32 bytes here, since this function should be + // really short (e.g a mov and a ret). + code := make([]byte, 32) + if _, err = ef.ReadVirtualMemory(code, int64(vmKindAddr)); err != nil { + return 0, fmt.Errorf("could not read from zend_vm_kind: %w", err) + } + + vmKind, err := retrieveZendVMKindWrapper(code) + if err != nil { + return 0, fmt.Errorf("an error occurred decoding zend_vm_kind: %w", err) + } + + return vmKind, nil +} + +func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + if !phpRegex.MatchString(info.FileName()) { + return nil, nil + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + version, err := determinePHPVersion(ef) + if err != nil { + return nil, err + } + + // Only tested on PHP7.3-PHP8.1. Other similar versions probably only require + // tweaking the offsets. + const minVer, maxVer = 0x070300, 0x080300 + 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, + (minVer>>16)&0xff, (minVer>>8)&0xff, + (maxVer>>16)&0xff, (maxVer>>8)&0xff) + } + + egAddr, err := ef.LookupSymbolAddress("executor_globals") + if err != nil { + return nil, fmt.Errorf("PHP %x: executor_globals not found: %v", version, err) + } + + // Zend/zend_vm_execute.h: execute_ex(zend_execute_data *ex) is the main VM + // executor function, has been such at least since PHP7.0. + interpRanges, err := info.GetSymbolAsRanges("execute_ex") + if err != nil { + return nil, err + } + + // If the version is PHP8+ we need to be able to + // potentially unwind JIT code. For now we need to recover + // the return address and check that hybrid mode is used. This is the + // default mode (there are others but they should be rarely used in production) + // 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 { + var vmKind uint + vmKind, err = determineVMKind(ef) + if err != nil { + log.Debugf("PHP version %x: an error occurred while determining "+ + "the VM kind (%v)", + version, err) + } else if vmKind == ZEND_VM_KIND_HYBRID { + rtAddr, err = recoverExecuteExJumpLabelAddress(ef) + if err != nil { + log.Debugf("PHP version %x: an error occurred while determining "+ + "the return address for execute_ex: (%v)", version, err) + } + } + } + pid := &php7Data{ + version: version, + egAddr: libpf.Address(egAddr), + rtAddr: libpf.Address(rtAddr), + } + + // PHP 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 := &pid.vmStructs + vms.zend_executor_globals.current_execute_data = 488 + vms.zend_execute_data.opline = 0 + vms.zend_execute_data.function = 24 + vms.zend_execute_data.this_type_info = 40 + vms.zend_execute_data.prev_execute_data = 48 + vms.zend_function.common_type = 0 + vms.zend_function.common_funcname = 8 + vms.zend_function.op_array_filename = 128 + vms.zend_function.op_array_linestart = 136 + // Note: the sizeof here isn't actually the sizeof the + // zend_function object. This is set to 168 + // primarily for efficiency reasons, since we + // need at most 168 bytes. + vms.zend_function.Sizeof = 168 + vms.zend_string.val = 24 + vms.zend_op.lineno = 24 + if version >= 0x080200 { + vms.zend_function.op_array_filename = 152 + vms.zend_function.op_array_linestart = 160 + } else if version >= 0x080000 { + vms.zend_function.op_array_filename = 144 + vms.zend_function.op_array_linestart = 152 + } else if version >= 0x070400 { + vms.zend_function.op_array_filename = 136 + vms.zend_function.op_array_linestart = 144 + } + + if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindPHP, + info.FileID(), interpRanges); err != nil { + return nil, err + } + + return pid, nil +} diff --git a/interpreter/php/php_test.go b/interpreter/php/php_test.go new file mode 100644 index 00000000..ac09596b --- /dev/null +++ b/interpreter/php/php_test.go @@ -0,0 +1,66 @@ +/* + * 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 "testing" + +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) + } + } + + 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) + } + } +} + +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}, + "suffix": { + given: "8.1.2-1ubuntu2.14", + expected: version(8, 1, 2), + expectError: false, + }, + "no-release": {given: "7.4", expected: 0, expectError: true}, + "trailing-dot": {given: "8.0.", expected: 0, expectError: true}, + "only-major": {given: "8", expected: 0, expectError: true}, + } + + for name, test := range tests { + 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) + } + }) + } +} diff --git a/interpreter/php/phpjit/opcache.go b/interpreter/php/phpjit/opcache.go new file mode 100644 index 00000000..2002bf87 --- /dev/null +++ b/interpreter/php/phpjit/opcache.go @@ -0,0 +1,387 @@ +/* + * 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 phpjit + +// nolint:lll +// PHP8+ JIT compiler unwinder. +// This file contains the code necessary for unwinding PHP code that has been JIT compiled. +// +// TL;DR: This file exists just to provide the PHP unwinder with the right PIDPages for JIT'd code. +// Everything else in interpreterphp works as expected for unwinding this code. +// +// It turns out that the PHP JIT compiler is a little bit strange compared to other JIT compilers +// (e.g V8) because of the unique limitations of the PHP compiler (or, rather, over 20 years of +// organic code growth). +// +// If you want to understand how the PHP JIT compiler actually works, there's no substitute +// for reading the PHP source code documents. In particular, these are useful: +// +// 1) https://github.com/php/php-src/blob/PHP-8.0/ext/opcache/jit/zend_jit.h +// 2) https://github.com/php/php-src/blob/PHP-8.0/ext/opcache/jit/zend_jit.c (if you have a while) +// +// It might also be useful to know how Zend Extensions work (or, at least be aware of their existence) +// It turns out the way these have been structured implies almost everything you need to know about +// why the JIT compiler is the way it is. This is a useful resource for understanding them +// https://www.phpinternalsbook.com/php7/extensions_design/zend_extensions.html +// +// The PHP JIT compiler uses dynasm for the actual JITing portion of the code. The informal +// tutorial is really good, you should read it (https://corsix.github.io/dynasm-doc/tutorial.html) +// You don't need to understand how Dynasm actually works for understanding this code, but it +// still might be useful for other projects. +// +// Before we begin it's illustrative to understand how PHP works internally. +// PHP belongs to the class of interpreted languages that use bytecode: each PHP function is +// decomposed into a sequence of bytecode instructions that are then executed by the PHP interpreter. +// These instructions are known as zend_ops in the Zend compiler, and their internal structure +// looks like this: +// struct _zend_op { +// const void *handler; +// znode_op op1; +// znode_op op2; +// znode_op result; +// .... +// }; +// +// Here the "handler" member is a pointer to some function that is executed when the +// zend_op is evaluated. Typically this is a PHP function of some kind that has already been +// pre-built. You can imagine this as a function pointer to some function that +// accepts two arguments and produces a singular result. Note that znode_ops can refer to other _zend_ops, +// which allows you to build a AST. +// A good resource on this (with far more detail, if you need it) is +// https://www.npopov.com/2017/04/14/PHP-7-Virtual-machine.html +// +// The way that the JIT compiler works is that it replaces the handler pointer with the address +// of some JIT'd code. +// In other words, there's some code somewhere that does something like this: +// my_op->handler = &some_function; +// (Or, exactly, this: https://github.com/php/php-src/blob/PHP-8.0/ext/opcache/jit/zend_jit.c#L427) +// This means that when the zend_op is evaluated native code is used +// rather than the PHP function, which enables the code to be much faster. +// +// The implication of this is that the PHP unwinder doesn't actually need to be changed at all (beyond a few offsets) +// since the executor_globals is still the primary point of execution[1]. This means that all we have to do +// is tell the eBPF code to unwind PHP when JIT'd code is encountered. +// +// However, getting the JIT memory regions inside the base PHP interpreter is difficult. +// It turns out that PHP's JIT compiler is a bit strange: the memory for the JIT'd code lives inside a +// Zend extension called the OPCache. +// +// As justification: older versions of PHP (e.g before PHP 5.5) had a problem: each time a script was executed +// the script needed to be parsed into opcodes, compiled on the virtual machine and then +// executed. This is rather inefficient for frequently called scripts: so, PHP 5.5 introduced +// the OPcache, which caches frequently used scripts and the corresponding opcodes. +// It turns out that the makers of the Zend engine reasoned that if you wanted the JIT you'd also want the Opcache. +// +// It's natural to ask where the Opcache lives in memory. Since PHP supports +// both thread and process-level parallelism, this memory needs to be shared across all PHP +// processes. This means that the Opcache doesn't live in any single processes memory; it's actually +// allocated in shared memory. +// +// The implication of this are: +// a) JIT'd code lives in shared memory, which means that all of the process-local work that the host-agent normally does doesn't really apply for PHP. +// b) The PHP JIT doesn't even live in the same shared object as the PHP interpreter, so we can't +// find the JIT information from the PHP interpreter. +// c) Even if we could, PHP hides symbols by default and so recovering the relevant information isn't easy in this form[2]. +// +// Note that we also can't use the approach used in the V8 interpreter +// because the JIT'd PHP code doesn't ever get loaded as an anonymous mapping (the JIT region is just +// marked as executable and it's never loaded into the executable directly). +// In a sense this is more like how the Hotspot Interpreter works. +// +// The solution for this problem we use here is to resolve the OPcache mapping for the PHP process. +// The reasons for this are: +// a) The OPcache contains symbols for the externally-exposed JIT functions. +// b) At least one of those functions sets a both a pointer to the JIT memory and a variable +// that contains the size of the buffer. +// c) The OPcache is the shared object that actually allocates the memory: when the OPcache extension +// is initialized the memory is allocated. This also just makes everything a bit neater. +// +// This means that we can inform the eBPF code that the PHP unwinder should be used whenever JIT'd PHP code is encountered. +// +// The design of this interpreter is therefore as follows: we don't do _any_ PHP unwinding in +// this interpreter at all. This interpreter is solely meant to allow the PHP unwinder to be triggered +// when appropriate. This means that the interpreter is really basic compared to all of the other +// interpreters. +// +// Footnotes: +// (1) In different modes there are other approaches that you can use to do this sort of unwinding. For example, in Debug mode the Zend compiler stores +// information about each JIT'd frames in a jit_globals structure. This is probably really useful if the client is running in Debug mode (or if they've +// compiled PHP with HAVE_GDB enabled or similar) but this isn't guaranteed to work across all deployments (whereas this version should). +// (2) The original version (i.e pre-PR) code tried to do this. You can (in theory) walk the module registry that PHP provides to find the JIT info at runtime +// and then you can do this all inside interpreterphp. However, this turned out to be really complicated, brittle and less efficient than this approach (there were +// far more memory reads from the particular process during the initial loading than with this approach). +// (3) Note that this code should also work with PHP's thread-safe resource management mechanism. Since the JIT buffer is shared across all processes anyway clients who +// use the TSRM shouldn't encounter any issues here. There are other uncommon ways to build PHP, but these also shouldn't affect how this code works. + +import ( + "encoding/binary" + "fmt" + "regexp" + "unsafe" + + "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/reporter" + "github.com/elastic/otel-profiling-agent/support" + + log "github.com/sirupsen/logrus" + "go.uber.org/multierr" +) + +// #include "../../../support/ebpf/types.h" +import "C" + +var ( + // Regex from the opcache. + opcacheRegex = regexp.MustCompile(`^(?:.*/)?opcache\.so$`) + // Make sure that the needed interfaces are satisfied + _ interpreter.Data = &opcacheData{} + _ interpreter.Instance = &opcacheInstance{} +) + +type opcacheData struct { + version uint + + // dasmBuf is the address of the shared memory that is used for the JIT'd code. + // This is defined here: + // https://github.com/php/php-src/blob/PHP-8.0/ext/opcache/jit/zend_jit.c#L103 + dasmBufPtr libpf.Address + + // dasmSize is the size of the JIT buffer. + // This is defined here: + // https://github.com/php/php-src/blob/PHP-8.0/ext/opcache/jit/zend_jit.c#L107 + dasmSizePtr libpf.Address +} + +type opcacheInstance struct { + interpreter.InstanceStubs + + // d is the interpreter data from opcache.so (shared between processes) + d *opcacheData + + // rm is used to access the remote process memory + rm remotememory.RemoteMemory + + // bias is the load bias + bias libpf.Address + + // prefixes is the list of LPM prefixes added to ebpf maps (to be cleaned up) + prefixes []lpm.Prefix +} + +func (i *opcacheInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.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", + prefix.Key, prefix.Length, err2)) + } + } + + if err != nil { + return fmt.Errorf("failed to detach opcacheInstance from PID %d: %w", + pid, err) + } + + return nil +} + +func (i *opcacheInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, + _ reporter.SymbolReporter, pr process.Process, _ []process.Mapping) error { + if i.prefixes != nil { + // Already attached + return nil + } + + dasmBufVal := make([]byte, 8) + dasmSizeVal := make([]byte, 8) + if err := i.rm.Read(i.d.dasmBufPtr+i.bias, dasmBufVal); err != nil { + return nil + } + if err := i.rm.Read(i.d.dasmSizePtr+i.bias, dasmSizeVal); err != nil { + return nil + } + + dasmBuf := binary.LittleEndian.Uint64(dasmBufVal) + dasmSize := binary.LittleEndian.Uint64(dasmBufVal) + if dasmBuf == 0 || dasmSize == 0 { + // This is the normal path if JIT is not enabled, or we try to + // attach before JIT engine is initialized. + return nil + } + + prefixes, err := lpm.CalculatePrefixList(dasmBuf, dasmBuf+dasmSize) + if err != nil { + log.Debugf("Producing prefixes failed: %v", err) + 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) + if err != nil { + return err + } + } + return nil +} + +func (d *opcacheData) String() string { + ver := d.version + 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, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + return &opcacheInstance{ + d: d, + rm: rm, + bias: bias, + }, nil +} + +func determineOPCacheVersion(ef *pfelf.File) (uint, error) { + // In contrast to interpreterphp, the opcache actually contains + // a really straightforward way to recover the version. As the opcache + // is a Zend extension, it has to provide a version, which just so + // happens to be the PHP version. + // + // The way this function works is as follows. + // Each zend_extension in PHP looks something like this: + // + // Courtesy of https://github.com/php/php-src/blob/PHP-8.0/Zend/zend_extensions.h#L77, + // but the structure's layout hasn't changed in 20+ years + // + // struct _zend_extension { + // char *name; + // char *version; + // ... + // }; + // + // Now normally this could be anything: anyone can write a zend extension. + // However, for the opcache in particular this is exactly the PHP version: + // + // https://github.com/php/php-src/blob/PHP-8.0/ext/opcache/ZendAccelerator.c#L4994 + // + // ZEND_EXT_API zend_extension zend_extension_entry = { + // ACCELERATOR_PRODUCT_NAME, + // PHP_VERSION, + // ... + // }; + // + // Since the version is the PHP_VERSION, we can just recover the version by reading + // the second pointer in the struct and parsing that. This has been the case since + // PHP 7.0, which is good enough. + + moduleExtension, err := ef.LookupSymbolAddress("zend_extension_entry") + if err != nil { + return 0, fmt.Errorf("could not find zend_extension_entry: %w", err) + } + + // 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) { + 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) +} + +// getOpcacheJITInfo retrieves the starting address and the size of the JIT buffer. +// If these cannot be found then (libpf.SymbolValueInvalid, 0, error) +// will be returned. +func getOpcacheJITInfo(ef *pfelf.File) (dasmBuf, dasmSize libpf.Address, err error) { + // This function works by disassembling a particular exported function and + // using that to recover the relevant information. + // The steps are as follows: + // a) Disassemble zend_jit_unprotect. + // b) Recover the address of dasm_buf and the size of the buffer. + // Note: zend_jit_unprotect was chosen because it immediately calls mprotect with + // dasm_buf as the first parameter, which should be in a register for both x86-64 + // and ARM64. + zendJit, err := ef.LookupSymbolAddress("zend_jit_unprotect") + if err != nil { + return 0, 0, err + } + + // We should only need 64 bytes, since this should be early in the instruction sequence. + code := make([]byte, 64) + if _, err = ef.ReadVirtualMemory(code, int64(zendJit)); err != nil { + return 0, 0, err + } + + dasmBufPtr, dasmSizePtr, err := php.RetrieveJITBufferPtrWrapper(code, zendJit) + if err != nil { + return 0, 0, fmt.Errorf("failed to extract DASM pointers: %w", err) + } + if dasmBufPtr == libpf.SymbolValueInvalid || dasmBufPtr%4 != 0 { + return 0, 0, fmt.Errorf("dasmBufPtr %#x is invalid", dasmBufPtr) + } + if dasmSizePtr == libpf.SymbolValueInvalid || dasmSizePtr%4 != 0 { + return 0, 0, fmt.Errorf("bad dasmSizePtr %#x is invalid", dasmSizePtr) + } + return libpf.Address(dasmBufPtr), libpf.Address(dasmSizePtr), nil +} + +func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + if !opcacheRegex.MatchString(info.FileName()) { + return nil, nil + } + + ef, err := info.GetELF() + if err != nil { + return nil, fmt.Errorf("could not get ELF: %w", err) + } + + // Determine PHP version first + version, err := determineOPCacheVersion(ef) + if err != nil { + return nil, err + } + + // Expect PHP 8+ for proper JIT support + if version < 0x080000 { + return nil, nil + } + + // Extract location from where to read dasm buffer + dasmBufPtr, dasmSizePtr, err := getOpcacheJITInfo(ef) + if err != nil { + return nil, err + } + + // We only load the JIT buffer address in Attach: this is because + // we might need to spin on the buffer being available. + pid := &opcacheData{ + version: version, + dasmBufPtr: dasmBufPtr, + dasmSizePtr: dasmSizePtr, + } + + return pid, nil +} diff --git a/interpreter/python/decode_amd64.c b/interpreter/python/decode_amd64.c new file mode 100644 index 00000000..f834fa01 --- /dev/null +++ b/interpreter/python/decode_amd64.c @@ -0,0 +1,79 @@ +//go:build amd64 + +#include +#include "decode_amd64.h" + +#include + +// decode_stub_argument() will decode instructions from given code blob until an assignment +// for the given argument register is found. The value loaded is then determined from the +// opcode. A call/jump instruction will terminate the finding as we are finding the argument +// to first function call (or tail call). +// Currently the following addressing schemes for the assignment are supported: +// 1) Loading virtual address with immediate value. This happens for non-PIC globals. +// 2) Loading RIP-relative virtual address. Happens for PIC/PIE globals. +// 3) Loading via pointer + displacement. Happens when the main state is given as argument, +// and the value is loaded from it. In this case 'memory_base' should be the address of +// the global state variable. +uint64_t decode_stub_argument(const uint8_t* code, size_t codesz, uint8_t argument_no, + uint64_t rip_base, uint64_t memory_base) { + ZydisDecoder decoder; + ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64); + + // Argument number to x86_64 calling convention register mapping. + ZydisRegister target_register64, target_register32; + switch (argument_no) { + case 0: + target_register64 = ZYDIS_REGISTER_RDI; + target_register32 = ZYDIS_REGISTER_EDI; + break; + case 1: + target_register64 = ZYDIS_REGISTER_RSI; + target_register32 = ZYDIS_REGISTER_ESI; + break; + case 2: + target_register64 = ZYDIS_REGISTER_RDX; + target_register32 = ZYDIS_REGISTER_EDX; + break; + default: + return 0; + } + + // Iterate instructions + ZydisDecodedInstruction instr; + ZyanUSize instruction_offset = 0; + while (ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + instruction_offset, + codesz - instruction_offset, &instr))) { + instruction_offset += instr.length; + if (instr.mnemonic == ZYDIS_MNEMONIC_CALL || + instr.mnemonic == ZYDIS_MNEMONIC_JMP) { + // Unexpected call/jmp indicating end of stub code + return 0; + } + if (!(instr.mnemonic == ZYDIS_MNEMONIC_LEA || + instr.mnemonic == ZYDIS_MNEMONIC_MOV) || + instr.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER || + (instr.operands[0].reg.value != target_register64 && + instr.operands[0].reg.value != target_register32)) { + // Only "LEA/MOV target_reg, ..." meaningful + continue; + } + if (instr.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) { + // MOV target_reg, immediate + return instr.operands[1].imm.value.u; + } + if (instr.operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY && + instr.operands[1].mem.disp.has_displacement) { + if (instr.operands[1].mem.base == ZYDIS_REGISTER_RIP) { + // MOV/LEA target_reg, [RIP + XXXX] + return rip_base + instruction_offset + instr.operands[1].mem.disp.value; + } else if (memory_base) { + // MOV/LEA target_reg, [REG + XXXX] + return memory_base + instr.operands[1].mem.disp.value; + } + continue; + } + } + + return 0; +} diff --git a/interpreter/python/decode_amd64.go b/interpreter/python/decode_amd64.go new file mode 100644 index 00000000..c495a64c --- /dev/null +++ b/interpreter/python/decode_amd64.go @@ -0,0 +1,28 @@ +//go:build amd64 + +/* + * 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 ( + "unsafe" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +// #cgo CFLAGS: -g -Wall +// #cgo LDFLAGS: -lZydis +// #include "decode_amd64.h" +// #include "../../support/ebpf/types.h" +import "C" + +func decodeStubArgumentWrapper(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))) +} diff --git a/interpreter/python/decode_amd64.h b/interpreter/python/decode_amd64.h new file mode 100644 index 00000000..f5ad7ca4 --- /dev/null +++ b/interpreter/python/decode_amd64.h @@ -0,0 +1,10 @@ +//go:build amd64 + +#ifndef __PYTHON_DECODE_X86_64__ +#define __PYTHON_DECODE_X86_64__ + +#include + +uint64_t decode_stub_argument(const uint8_t* code, size_t codesz, uint8_t argument_no, uint64_t rip_base, uint64_t memory_base); + +#endif diff --git a/interpreter/python/decode_arm64.go b/interpreter/python/decode_arm64.go new file mode 100644 index 00000000..7b8308d5 --- /dev/null +++ b/interpreter/python/decode_arm64.go @@ -0,0 +1,103 @@ +//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. + * See the file "LICENSE" for details. + */ + +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 +} diff --git a/interpreter/python/decode_arm64_test.go b/interpreter/python/decode_arm64_test.go new file mode 100644 index 00000000..2f3b4d8b --- /dev/null +++ b/interpreter/python/decode_arm64_test.go @@ -0,0 +1,34 @@ +//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. + * See the file "LICENSE" for details. + */ + +package python + +import ( + "testing" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/stretchr/testify/assert" +) + +func TestAnalyzeArm64Stubs(t *testing.T) { + val := decodeStubArgumentWrapper( + []byte{ + 0x40, 0x0a, 0x00, 0x90, 0x01, 0xd4, 0x43, 0xf9, + 0x22, 0x60, 0x17, 0x91, 0x40, 0x00, 0x40, 0xf9, + 0xa2, 0xff, 0xff, 0x17}, + 0, 0, 0) + assert.Equal(t, libpf.SymbolValue(1496), val, "PyEval_ReleaseLock stub test") + + val = decodeStubArgumentWrapper( + []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") +} diff --git a/interpreter/python/python.go b/interpreter/python/python.go new file mode 100644 index 00000000..85ccff25 --- /dev/null +++ b/interpreter/python/python.go @@ -0,0 +1,854 @@ +/* + * 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 ( + "debug/elf" + "fmt" + "hash/fnv" + "reflect" + "regexp" + "strconv" + "strings" + "sync/atomic" + "unsafe" + + 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/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/support" + "github.com/elastic/otel-profiling-agent/tpbase" +) + +// #include +// #include "../../support/ebpf/types.h" +import "C" + +// The following regexs are intended to match either a path to a Python binary or +// library. +var ( + pythonRegex = regexp.MustCompile(`^(?:.*/)?python(\d)\.(\d+)(d|m|dm)?$`) + libpythonRegex = regexp.MustCompile(`^(?:.*/)?libpython(\d)\.(\d+)[^/]*`) +) + +// nolint:lll +type pythonData struct { + version uint16 + + autoTLSKey libpf.SymbolValue + + // vmStructs reflects the Python Interpreter introspection data we want + // need to extract data from the runtime. The fields are named as they are + // in the Python code. Eventually some of these fields will be read from + // the Python introspection data, and matched using the reflection names. + vmStructs struct { + // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/cpython/object.h#L148 + PyTypeObject struct { + BasicSize libpf.Address `name:"tp_basicsize"` + Members libpf.Address `name:"tp_members"` + } + // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/structmember.h#L18 + PyMemberDef struct { + Sizeof libpf.Address + Name uint `name:"name"` + Offset uint `name:"offset"` + } + // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/cpython/unicodeobject.h#L72 + PyASCIIObject struct { + Data uint `name:"data"` + } + PyCodeObject struct { + Sizeof uint + ArgCount uint `name:"co_argcount"` + KwOnlyArgCount uint `name:"co_kwonlyargcount"` + Flags uint `name:"co_flags"` + FirstLineno uint `name:"co_firstlineno"` + Filename uint `name:"co_filename"` + Name uint `name:"co_name"` + Lnotab uint `name:"co_lnotab"` + Linetable uint `name:"co_linetable"` // Python 3.10+ + QualName uint `name:"co_qualname"` // Python 3.11+ + } + // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/object.h#L109 + PyVarObject struct { + ObSize uint `name:"ob_size"` + } + PyBytesObject struct { + Sizeof uint + } + // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/cpython/pystate.h#L82 + PyThreadState 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"` + } + // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/cpython/pystate.h#L38 + PyCFrame struct { + CurrentFrame uint `name:"current_frame"` + } + } +} + +var _ interpreter.Data = &pythonData{} + +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, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + addrToCodeObject, err := + freelru.New[libpf.Address, *pythonCodeObject](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + i := &pythonInstance{ + d: d, + rm: rm, + bias: C.u64(bias), + addrToCodeObject: addrToCodeObject, + } + + switch d.version { + case 0x030b: + i.getFuncOffset = walkLocationTable + case 0x030a: + i.getFuncOffset = walkLineTable + default: + i.getFuncOffset = mapByteCodeIndexToLine + } + + return i, nil +} + +// pythonCodeObject contains the information we cache for a corresponding +// Python interpreter's PyCodeObject structures. +type pythonCodeObject struct { + // As of Python 3.10 elements of PyCodeObject have changed and so we need + // to handle them differently. To be able to do so we keep track of the python version. + version uint16 + + // name is the extracted co_name (the unqualified method or function name) + name string + + // sourceFileName is the extracted co_filename field + sourceFileName string + + // For Python version < 3.10 lineTable is the extracted co_lnotab, and contains the + // "bytecode index" to "line number" mapping data. + // For Python version >= 3.10 lineTable is the extracted co_linetable. + lineTable []byte + + // firstLineNo is the extracted co_firstlineno field, and contains the line + // number where the method definition in source code starts + firstLineNo uint32 + + // ebpfChecksum is the simple hash of few PyCodeObject fields sent from eBPF + // to verify that the data we extracted from remote process is still valid + ebpfChecksum uint32 + + // fileID is a more complete hash of various PyCodeObject fields, which is + // used as the global ID of the PyCodeObject. It is stored as the FileID + // part of the Frame in the DB. + fileID libpf.FileID + + // bciSeen is a set of "lastI" or byte code index (bci) values we have + // already symbolized and sent to the collection agent + 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 { + return 0 + } + nextVal = lt[i] + i++ + shift += 6 + val |= uint(nextVal&63) << shift + } + return val +} + +// 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] + + code = (firstByte >> 3) & 15 + + // Handle the 16 possible different codes known as _PyCodeLocationInfoKind. + // nolint:lll + // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/cpython/code.h#L219 + 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 + case 10, 11, 12: + // PY_CODE_LOCATION_INFO_ONE_LINE embeds the line information in the code. + steps = 2 + line += int(code - 10) + case 13: + // PY_CODE_LOCATION_INFO_NO_COLUMNS + steps = 1 + if i+1 >= lenLineTable { + return 0 + } + diff := readSignedVarint(lineTable[i+1:]) + line += diff + case 14: + // PY_CODE_LOCATION_INFO_LONG + steps = 4 + if i+1 >= lenLineTable { + return 0 + } + diff := readSignedVarint(lineTable[i+1:]) + line += diff + case 15: + // PY_CODE_LOCATION_INFO_NONE does not hold line information + steps = 0 + 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 + } + + return uint32(line) +} + +// walkLineTable implements the algorithm to walk the line number table that was introduced +// with Python 3.10. While firstLineNo still holds the line number of the function, the line +// number table extends this information with the offset into this function. +func walkLineTable(m *pythonCodeObject, addrq uint32) uint32 { + // The co_linetab format is specified in python Objects/lnotab_notes.txt + if addrq == 0 { + return 0 + } + lineTable := m.lineTable + var line, start, end uint32 + for i := 0; i < len(lineTable)/2; i += 2 { + sDelta := lineTable[i] + lDelta := int8(lineTable[i+1]) + if lDelta == 0 { + end += uint32(sDelta) + continue + } + start = end + end = start + uint32(sDelta) + if lDelta == -128 { + // A line delta of -128 is a special indicator mentioned in + // Objects/lnotab_notes.txt and indicates an invalid line number. + continue + } + line += uint32(lDelta) + if end == start { + continue + } + if end > addrq { + return line + } + } + return 0 +} + +func mapByteCodeIndexToLine(m *pythonCodeObject, bci uint32) uint32 { + // The co_lntab format is specified in python Objects/lnotab_notes.txt + lineno := uint32(0) + addr := uint(0) + // The lnotab length is checked to be even before it's extracted in getCodeObject() + lnotab := m.lineTable + for i := 0; i < len(lnotab); i += 2 { + addr += uint(lnotab[i]) + if addr > uint(bci) { + return lineno + } + lineno += uint32(lnotab[i+1]) + if lnotab[i+1] >= 0x80 { + lineno -= 0x100 + } + } + return lineno +} + +func (m *pythonCodeObject) symbolize(symbolizer interpreter.Symbolizer, bci uint32, + getFuncOffset getFuncOffsetFunc, trace *libpf.Trace) error { + trace.AppendFrame(libpf.PythonFrame, m.fileID, libpf.AddressOrLineno(bci)) + + // Check if this is already symbolized + if _, ok := m.bciSeen[bci]; ok { + return nil + } + + var lineNo libpf.SourceLineno + functionOffset := getFuncOffset(m, bci) + lineNo = libpf.SourceLineno(m.firstLineNo + functionOffset) + + symbolizer.FrameMetadata(m.fileID, + libpf.AddressOrLineno(bci), lineNo, functionOffset, + m.name, m.sourceFileName) + + // FIXME: The above FrameMetadata 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[bci] = libpf.Void{} + + log.Debugf("[%d] [%x] %v+%v at %v:%v", len(trace.FrameTypes), + m.fileID, + m.name, functionOffset, + m.sourceFileName, lineNo) + + return nil +} + +// getFuncOffsetFunc provides functionality to return a function offset from a PyCodeObject +type getFuncOffsetFunc func(m *pythonCodeObject, bci uint32) uint32 + +type pythonInstance struct { + interpreter.InstanceStubs + + // Python symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + + d *pythonData + rm remotememory.RemoteMemory + bias C.u64 + + // addrToCodeObject maps a Python Code object to a pythonCodeObject which caches + // the needed data from it. + addrToCodeObject *freelru.LRU[libpf.Address, *pythonCodeObject] + + // getFuncOffset provides fast access in order to get the function offset for different + // Python interpreter versions. + getFuncOffset getFuncOffsetFunc + + // procInfoInserted tracks whether we've already inserted process info into BPF maps. + procInfoInserted bool +} + +var _ interpreter.Instance = &pythonInstance{} + +func (p *pythonInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + addrToCodeObjectStats := p.addrToCodeObject.GetAndResetStatistics() + + return []metrics.Metric{ + { + ID: metrics.IDPythonSymbolizationSuccesses, + Value: metrics.MetricValue(p.successCount.Swap(0)), + }, + { + ID: metrics.IDPythonSymbolizationFailures, + Value: metrics.MetricValue(p.failCount.Swap(0)), + }, + { + ID: metrics.IDPythonAddrToCodeObjectHit, + Value: metrics.MetricValue(addrToCodeObjectStats.Hit), + }, + { + ID: metrics.IDPythonAddrToCodeObjectMiss, + Value: metrics.MetricValue(addrToCodeObjectStats.Miss), + }, + { + ID: metrics.IDPythonAddrToCodeObjectAdd, + Value: metrics.MetricValue(addrToCodeObjectStats.Added), + }, + { + ID: metrics.IDPythonAddrToCodeObjectDel, + Value: metrics.MetricValue(addrToCodeObjectStats.Deleted), + }, + }, nil +} + +func (p *pythonInstance) UpdateTSDInfo(ebpf interpreter.EbpfHandler, pid libpf.PID, + tsdInfo tpbase.TSDInfo) error { + d := p.d + vm := &d.vmStructs + cdata := C.PyProcInfo{ + autoTLSKeyAddr: C.u64(d.autoTLSKey) + p.bias, + version: C.u16(d.version), + + tsdInfo: C.TSDInfo{ + offset: C.s16(tsdInfo.Offset), + multiplier: C.u8(tsdInfo.Multiplier), + indirect: C.u8(tsdInfo.Indirect), + }, + + PyThreadState_frame: C.u8(vm.PyThreadState.Frame), + PyCFrame_current_frame: C.u8(vm.PyCFrame.CurrentFrame), + 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), + 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), + } + + err := ebpf.UpdateProcData(libpf.Python, pid, unsafe.Pointer(&cdata)) + if err != nil { + return err + } + + p.procInfoInserted = true + return err +} + +func (p *pythonInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { + if !p.procInfoInserted { + return nil + } + + err := ebpf.DeleteProcData(libpf.Python, pid) + if err != nil { + return fmt.Errorf("failed to detach pythonInstance from PID %d: %v", + pid, err) + } + return nil +} + +// frozenNameToFileName convert special Python file names into real file names. +// Return the new file name or the unchanged input if it wasn't a frozen file name +// or the format was not as expected. +// +// Examples seen regularly with python3.7 and python3.8: +// +// "" --> "_bootstrap.py" +// "" --> "_bootstrap_external.py" +func frozenNameToFileName(sourceFileName string) (string, error) { + if !strings.HasPrefix(sourceFileName, "= 0x10000 || (p.d.version < 0x30b && lineTableSize&1 != 0) { + return nil, fmt.Errorf("invalid line table size (%v)", lineTableSize) + } + lineTable := make([]byte, lineTableSize) + err = p.rm.Read(lineInfoPtr+libpf.Address(vms.PyBytesObject.Sizeof)-1, lineTable) + if err != nil { + return nil, fmt.Errorf("failed to read line table: %v", err) + } + + // 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(name)) + _, _ = h.Write(cobj[vms.PyCodeObject.FirstLineno : vms.PyCodeObject.FirstLineno+4]) + _, _ = h.Write(cobj[vms.PyCodeObject.ArgCount : vms.PyCodeObject.ArgCount+4]) + _, _ = h.Write(cobj[vms.PyCodeObject.KwOnlyArgCount : vms.PyCodeObject.KwOnlyArgCount+4]) + _, _ = h.Write(lineTable) + fileID, err := libpf.FileIDFromBytes(h.Sum(nil)) + if err != nil { + return nil, fmt.Errorf("failed to create a file ID: %v", err) + } + + pco := &pythonCodeObject{ + version: p.d.version, + name: name, + sourceFileName: sourceFileName, + firstLineNo: firstLineNo, + lineTable: lineTable, + ebpfChecksum: ebpfChecksum, + fileID: fileID, + bciSeen: make(libpf.Set[uint32]), + } + p.addrToCodeObject.Add(addr, pco) + return pco, nil +} + +func (p *pythonInstance) Symbolize(symbolReporter reporter.SymbolReporter, + frame *host.Frame, trace *libpf.Trace) error { + if !frame.Type.IsInterpType(libpf.Python) { + return interpreter.ErrMismatchInterpreterType + } + + // Extract the Python frame bitfields from the file and line variables + ptr := libpf.Address(frame.File) + lastI := uint32(frame.Lineno>>32) & 0x0fffffff + objectID := uint32(frame.Lineno) + + sfCounter := successfailurecounter.New(&p.successCount, &p.failCount) + defer sfCounter.DefaultToFailure() + + // Extract and symbolize + method, err := p.getCodeObject(ptr, objectID) + if err != nil { + return fmt.Errorf("failed to get python object %x: %v", objectID, err) + } + + err = method.symbolize(symbolReporter, lastI, p.getFuncOffset, trace) + if err != nil { + return fmt.Errorf("failed to symbolize python object %x, lastI %v: %v", + objectID, lastI, err) + } + + sfCounter.ReportSuccess() + return nil +} + +// fieldByPythonName searches obj for a field by its Python name using the struct tags. +func fieldByPythonName(obj reflect.Value, fieldName string) 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 _, pythonName := range strings.Split(nameTag, ",") { + if fieldName == pythonName { + return obj.Field(i) + } + } + } + if fieldName == objField.Name { + return obj.Field(i) + } + } + return reflect.Value{} +} + +func (d *pythonData) readIntrospectionData(ef *pfelf.File, symbol libpf.SymbolName, + vmObj any) error { + typeData, err := ef.LookupSymbolAddress(symbol) + if err != nil { + return fmt.Errorf("symbol '%s' not found", symbol) + } + rm := ef.GetRemoteMemory() + vms := &d.vmStructs + typedataAddress := libpf.Address(typeData) + reflection := reflect.ValueOf(vmObj).Elem() + if f := reflection.FieldByName("Sizeof"); f.IsValid() { + size := rm.Uint64(typedataAddress + vms.PyTypeObject.BasicSize) + f.SetUint(size) + } + + membersPtr := rm.Ptr(typedataAddress + vms.PyTypeObject.Members) + if membersPtr == 0 { + return nil + } + + for addr := membersPtr; true; addr += vms.PyMemberDef.Sizeof { + memberName := rm.StringPtr(addr + libpf.Address(vms.PyMemberDef.Name)) + if memberName == "" { + break + } + if f := fieldByPythonName(reflection, memberName); f.IsValid() { + offset := rm.Uint32(addr + libpf.Address(vms.PyMemberDef.Offset)) + f.SetUint(uint64(offset)) + } + } + return nil +} + +// decodeStub will resolve a given symbol, extract the code for it, and analyze +// the code to resolve specified argument parameter to the first jump/call. +func decodeStub(ef *pfelf.File, addrBase libpf.SymbolValue, symbolName libpf.SymbolName, + argNumber uint8) libpf.SymbolValue { + symbolValue, err := ef.LookupSymbolAddress(symbolName) + if err != nil { + return libpf.SymbolValueInvalid + } + + code := make([]byte, 64) + if _, err := ef.ReadVirtualMemory(code, int64(symbolValue)); err != nil { + return libpf.SymbolValueInvalid + } + + value := decodeStubArgumentWrapper(code, argNumber, symbolValue, addrBase) + + // Sanity check the value range and alignment + if value%4 != 0 { + return libpf.SymbolValueInvalid + } + // If base symbol (_PyRuntime) is not provided, accept any found value. + if addrBase == 0 && value != 0 { + return value + } + // Check that the found value is within reasonable distance from the given symbol. + if value > addrBase && value < addrBase+4096 { + return value + } + return libpf.SymbolValueInvalid +} + +func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + mainDSO := false + matches := libpythonRegex.FindStringSubmatch(info.FileName()) + if matches == nil { + mainDSO = true + matches = pythonRegex.FindStringSubmatch(info.FileName()) + if matches == nil { + return nil, nil + } + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + if mainDSO { + var needed []string + needed, err = ef.DynString(elf.DT_NEEDED) + if err != nil { + return nil, err + } + for _, n := range needed { + if libpythonRegex.MatchString(n) { + // 'python' linked with 'libpython'. The beef is in the library, + // so do not try to inspect the shim main binary. + return nil, nil + } + } + } + + var pyruntimeAddr, autoTLSKey libpf.SymbolValue + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + version := uint16(major*0x100 + minor) + + const minVer, maxVer = 0x306, 0x30b + if version < minVer || version > maxVer { + return nil, fmt.Errorf("unsupported Python %d.%d (need >= %d.%d and <= %d.%d)", + major, minor, + (minVer>>8)&0xff, minVer&0xff, + (maxVer>>8)&0xff, maxVer&0xff) + } + + if version >= 0x307 { + if pyruntimeAddr, err = ef.LookupSymbolAddress("_PyRuntime"); err != nil { + return nil, fmt.Errorf("_PyRuntime not defined: %v", err) + } + } + + // 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") + } + if version >= 0x307 && 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. + // Technically it would be best to resolve the jmp/call destination, but + // finding the jump slot name requires fairly complex plt relocation parsing. + // Instead this assumes that the TLS key address should be addr%8==4. This + // is because Py_tss_t consists of two "int" types and we want the latter. + // The first "int" is guaranteed to be aligned to 8, because in struct _PyRuntime + // it follows a pointer field. + autoTLSKey += 4 + } + + // The Python main interpreter loop history in CPython git is: + // + // nolint:lll + // deaf509e8fc v3.11 2022-11-15 _PyEval_EvalFrameDefault(PyThreadState*,_PyInterpreterFrame*,int) + // bc2cdfc8157 v3.10 2022-11-15 _PyEval_EvalFrameDefault(PyThreadState*,PyFrameObject*,int) + // 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 { + return nil, err + } + } + + pd := &pythonData{ + version: version, + autoTLSKey: autoTLSKey, + } + vms := &pd.vmStructs + + // Introspection data not available for these structures + vms.PyTypeObject.BasicSize = 32 + vms.PyTypeObject.Members = 240 + vms.PyMemberDef.Name = 0 + vms.PyMemberDef.Offset = 16 + vms.PyMemberDef.Sizeof = 40 + + vms.PyASCIIObject.Data = 48 + vms.PyVarObject.ObSize = 16 + vms.PyThreadState.Frame = 24 + + if version >= 0x30b { + // 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 + + // frame got removed in PyThreadState but we can use cframe instead. + vms.PyThreadState.Frame = 56 + + vms.PyCFrame.CurrentFrame = 8 + } + + // Read the introspection data from objects types that have it + if err := pd.readIntrospectionData(ef, "PyCode_Type", &vms.PyCodeObject); err != nil { + return nil, err + } + if err := pd.readIntrospectionData(ef, "PyFrame_Type", &vms.PyFrameObject); err != nil { + return nil, err + } + if err := pd.readIntrospectionData(ef, "PyBytes_Type", &vms.PyBytesObject); err != nil { + return nil, err + } + + if err := ebpf.UpdateInterpreterOffsets(support.ProgUnwindPython, info.FileID(), + interpRanges); err != nil { + return nil, err + } + + return pd, nil +} diff --git a/interpreter/python/python_test.go b/interpreter/python/python_test.go new file mode 100644 index 00000000..bfc3c890 --- /dev/null +++ b/interpreter/python/python_test.go @@ -0,0 +1,97 @@ +/* + * 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 ( + "regexp" + "testing" +) + +func TestFrozenNameToFileName(t *testing.T) { + tests := map[string]struct { + frozen string + expect string + expectErr bool + }{ + "Frozen": { + frozen: "", + expect: "_bootstrap.py", + }, + "Frozen subdir": { + frozen: "", + expect: "_bootstrap.py", + }, + "Frozen broken": { + frozen: "", + expectErr: true, + }, + "empty": { + frozen: "", + expect: "", + }, + } + + for name, testcase := range tests { + name := name + testcase := testcase + t.Run(name, func(t *testing.T) { + out, err := frozenNameToFileName(testcase.frozen) + + if (err != nil) != testcase.expectErr { + t.Fatalf("Unexpected error return") + } + + if out != testcase.expect { + t.Fatalf("'%s' does not match expected output '%s'", out, testcase.expect) + } + }) + } +} + +func TestPythonRegexs(t *testing.T) { + shouldMatch := map[*regexp.Regexp][]string{ + pythonRegex: { + "python3.6", "./python3.6", "/foo/bar/python3.6", "./foo/bar/python3.6", + "python3.7", "./python3.7", "/foo/bar/python3.7", "./foo/bar/python3.7"}, + libpythonRegex: { + "libpython3.6", "./libpython3.6", "/foo/bar/libpython3.6", + "./foo/bar/libpython3.6", "/foo/bar/libpython3.6.so.1", + "/usr/lib64/libpython3.6m.so.1.0", + "libpython3.7", "./libpython3.7", "/foo/bar/libpython3.7", + "./foo/bar/libpython3.7", "/foo/bar/libpython3.7.so.1", + "/foo/bar/libpython3.7m.so.1"}, + } + + for regex, strings := range shouldMatch { + for _, s := range strings { + if !regex.MatchString(s) { + t.Fatalf("regex %s should match %s", regex.String(), s) + } + } + } + + shouldNotMatch := map[*regexp.Regexp][]string{ + pythonRegex: { + "foopython3.6", "pyt hon3.6", "pyth/on3.6", "python", + "foopython3.7", "pyt hon3.7", "pyth/on3.7", "python"}, + libpythonRegex: { + "foolibpython3.6", "lib python3.6", "lib/python3.6", + "foolibpython3.7", "lib python3.7", "lib/python3.7"}, + } + + for regex, strings := range shouldNotMatch { + for _, s := range strings { + if regex.MatchString(s) { + t.Fatalf("regex %s should not match %s", regex.String(), s) + } + } + } +} diff --git a/interpreter/ruby/ruby.go b/interpreter/ruby/ruby.go new file mode 100644 index 00000000..4f31b938 --- /dev/null +++ b/interpreter/ruby/ruby.go @@ -0,0 +1,973 @@ +/* + * 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 ruby + +import ( + "encoding/binary" + "fmt" + "hash/fnv" + "math/bits" + "regexp" + "runtime" + "strconv" + "sync" + "sync/atomic" + "unsafe" + + 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/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/reporter" + "github.com/elastic/otel-profiling-agent/support" +) + +// #include "../../support/ebpf/types.h" +import "C" + +const ( + // iseqCacheSize is the LRU size for caching Ruby instruction sequences for an interpreter. + // This should reflect the number of hot functions that are seen often in a trace. + iseqCacheSize = 1024 + // addrToStringSize is the LRU size for caching Ruby VM addresses to Ruby strings. + addrToStringSize = 1024 + + // rubyInsnInfoSizeLimit defines the limit up to which we will allocate memory for the + // binary search algorithm to get the line number. + rubyInsnInfoSizeLimit = 1 * 1024 * 1024 +) + +// nolint:lll +const ( + // RUBY_T_STRING + // https://github.com/ruby/ruby/blob/c149708018135595b2c19c5f74baf9475674f394/include/ruby/internal/value_type.h#L117 + rubyTString = 0x5 + + // RUBY_T_ARRAY + // https://github.com/ruby/ruby/blob/c149708018135595b2c19c5f74baf9475674f394/include/ruby/internal/value_type.h#L119 + rubyTArray = 0x7 + + // RUBY_T_MASK + // https://github.com/ruby/ruby/blob/c149708018135595b2c19c5f74baf9475674f394/include/ruby/internal/value_type.h#L142 + rubyTMask = 0x1f + + // RSTRING_NOEMBED + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/include/ruby/ruby.h#L978 + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/include/ruby/ruby.h#L855 + // 1 << 13 + rstringNoEmbed = 8192 + + // RARRAY_EMBED_FLAG + rarrayEmbed = 8192 + + // PATHOBJ_REALPATH + pathObjRealPathIdx = 1 +) + +var ( + // regex to identify the Ruby interpreter executable + rubyRegex = regexp.MustCompile(`^(?:.*/)?libruby(?:-.*)?\.so\.(\d)\.(\d)\.(\d)$`) + // regex to extract a version from a string + rubyVersionRegex = regexp.MustCompile(`^(\d)\.(\d)\.(\d)$`) + + // compiler check to make sure the needed interfaces are satisfied + _ interpreter.Data = &rubyData{} + _ interpreter.Instance = &rubyInstance{} +) + +// nolint:lll +type rubyData struct { + // currentCtxPtr is the `ruby_current_execution_context_ptr` symbol value which is needed by the + // eBPF program to build ruby backtraces. + currentCtxPtr libpf.Address + + // version of the currently used Ruby interpreter. + // major*0x10000 + minor*0x100 + release (e.g. 3.0.1 -> 0x30001) + version uint32 + + // vmStructs reflects the Ruby internal names and offsets of named fields. + // nolint:golint,stylecheck,revive + vmStructs struct { + // rb_execution_context_struct + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L843 + execution_context_struct struct { + vm_stack, vm_stack_size, cfp uint8 + } + + // rb_control_frame_struct + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L760 + control_frame_struct struct { + pc, iseq, ep uint8 + size_of_control_frame_struct uint8 + } + + // rb_iseq_struct + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L456 + iseq_struct struct { + body uint8 + } + + // rb_iseq_constant_body + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L311 + iseq_constant_body struct { + iseq_type, encoded, size, location, insn_info_body, insn_info_size, succ_index_table uint8 + size_of_iseq_constant_body uint16 + } + + // rb_iseq_location_struct + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L272 + iseq_location_struct struct { + pathobj, base_label uint8 + } + + // succ_index_table_struct + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L3420 + succ_index_table_struct struct { + small_block_ranks, block_bits, succ_part, succ_dict_block uint8 + size_of_succ_dict_block uint8 + } + + // iseq_insn_info_entry + // https://github.com/ruby/ruby/blob/4e0a512972cdcbfcd5279f1a2a81ba342ed75b6e/iseq.h#L212 + iseq_insn_info_entry struct { + position, line_no uint8 + size_of_position, size_of_line_no, size_of_iseq_insn_info_entry uint8 + } + + // RString + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/include/ruby/ruby.h#L988 + // https://github.com/ruby/ruby/blob/86ac17efde6cf98903513cac2538b15fc4ac80b2/include/ruby/internal/core/rstring.h#L196 + rstring_struct struct { + // NOTE: starting with Ruby 3.1 the `as.ary` field is now `as.embed.ary` + as_heap_ptr, as_ary uint8 + } + + // RArray + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/include/ruby/ruby.h#L1048 + rarray_struct struct { + as_heap_ptr, as_ary uint8 + } + + // size_of_immediate_table holds the size of the macro IMMEDIATE_TABLE_SIZE as defined in + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L3418 + size_of_immediate_table uint8 + + // size_of_value holds the size of the macro VALUE as defined in + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L1136 + size_of_value uint8 + + // rb_ractor_struct + // https://github.com/ruby/ruby/blob/5ce0d2aa354eb996cb3ca9bb944f880ff6acfd57/ractor_core.h#L82 + rb_ractor_struct struct { + running_ec uint16 + } + } +} + +func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + cdata := C.RubyProcInfo{ + version: C.u32(r.version), + + current_ctx_ptr: C.u64(r.currentCtxPtr + bias), + + vm_stack: C.u8(r.vmStructs.execution_context_struct.vm_stack), + vm_stack_size: C.u8(r.vmStructs.execution_context_struct.vm_stack_size), + cfp: C.u8(r.vmStructs.execution_context_struct.cfp), + + pc: C.u8(r.vmStructs.control_frame_struct.pc), + iseq: C.u8(r.vmStructs.control_frame_struct.iseq), + ep: C.u8(r.vmStructs.control_frame_struct.ep), + size_of_control_frame_struct: C.u8( + r.vmStructs.control_frame_struct.size_of_control_frame_struct), + + body: C.u8(r.vmStructs.iseq_struct.body), + + iseq_size: C.u8(r.vmStructs.iseq_constant_body.size), + iseq_encoded: C.u8(r.vmStructs.iseq_constant_body.encoded), + + size_of_value: C.u8(r.vmStructs.size_of_value), + + running_ec: C.u16(r.vmStructs.rb_ractor_struct.running_ec), + } + + if err := ebpf.UpdateProcData(libpf.Ruby, pid, unsafe.Pointer(&cdata)); err != nil { + return nil, err + } + + iseqBodyPCToFunction, err := freelru.New[rubyIseqBodyPC, *rubyIseq](iseqCacheSize, + hashRubyIseqBodyPC) + if err != nil { + return nil, err + } + + addrToString, err := freelru.New[libpf.Address, string](addrToStringSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + return &rubyInstance{ + r: r, + rm: rm, + iseqBodyPCToFunction: iseqBodyPCToFunction, + addrToString: addrToString, + memPool: sync.Pool{ + New: func() any { + buf := make([]byte, 512) + return &buf + }, + }, + }, nil +} + +// rubyIseqBodyPC holds a reported address to a iseq_constant_body and Ruby VM program counter +// combination and is used as key in the cache. +type rubyIseqBodyPC struct { + addr libpf.Address + pc uint64 +} + +func hashRubyIseqBodyPC(iseq rubyIseqBodyPC) uint32 { + h := iseq.addr.Hash() + h ^= hash.Uint64(iseq.pc) + return uint32(h) +} + +// rubyIseq stores information extracted from a iseq_constant_body struct. +type rubyIseq struct { + // sourceFileName is the extracted filename field + sourceFileName string + + // fileID is the synthesized methodID + fileID libpf.FileID + + // line of code in source file for this instruction sequence + line libpf.AddressOrLineno +} + +type rubyInstance struct { + interpreter.InstanceStubs + + // Ruby symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + + r *rubyData + rm remotememory.RemoteMemory + + // iseqBodyPCToFunction maps an address and Ruby VM program counter combination to extracted + // information from a Ruby instruction sequence object. + iseqBodyPCToFunction *freelru.LRU[rubyIseqBodyPC, *rubyIseq] + + // addrToString maps an address to an extracted Ruby String from this address. + addrToString *freelru.LRU[libpf.Address, string] + + // memPool provides pointers to byte arrays for efficient memory reuse. + memPool sync.Pool + + // maxSize is the largest number we did see in the last reporting interval for size + // in getRubyLineNo. + maxSize atomic.Uint32 +} + +func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { + return ebpf.DeleteProcData(libpf.Ruby, pid) +} + +// readRubyArrayDataPtr obtains the data pointer of a Ruby array (RArray). +// +// https://github.com/ruby/ruby/blob/95aff2146/include/ruby/internal/core/rarray.h#L87 +func (r *rubyInstance) readRubyArrayDataPtr(addr libpf.Address) (libpf.Address, error) { + flags := r.rm.Ptr(addr) + if flags&rubyTMask != rubyTArray { + return 0, fmt.Errorf("object at 0x%08X is not an array", addr) + } + + vms := &r.r.vmStructs + if flags&rarrayEmbed == rarrayEmbed { + return addr + libpf.Address(vms.rarray_struct.as_ary), nil + } + + p := r.rm.Ptr(addr + libpf.Address(vms.rarray_struct.as_heap_ptr)) + if p != 0 { + return 0, fmt.Errorf("heap pointer of array at 0x%08X is 0", addr) + } + + return addr, nil +} + +// readPathObjRealPath reads the realpath field from a Ruby iseq pathobj. +// +// Path objects are represented as either a Ruby string (RString) or a +// Ruby arrays (RArray) with 2 entries. The first field contains a relative +// path, the second one an absolute one. All Ruby types start with an RBasic +// object that contains a type tag that we can use to determine what variant +// we're dealing with. +// +// https://github.com/ruby/ruby/blob/4e0a51297/iseq.c#L217 +// https://github.com/ruby/ruby/blob/95aff2146/vm_core.h#L267 +// https://github.com/ruby/ruby/blob/95aff2146/vm_core.h#L283 +// https://github.com/ruby/ruby/blob/7127f39ba/vm_core.h#L321-L321 +func (r *rubyInstance) readPathObjRealPath(addr libpf.Address) (string, error) { + flags := r.rm.Ptr(addr) + switch flags & rubyTMask { + case rubyTString: + // nothing to do + case rubyTArray: + var err error + addr, err = r.readRubyArrayDataPtr(addr) + if err != nil { + return "", err + } + + addr += pathObjRealPathIdx * libpf.Address(r.r.vmStructs.size_of_value) + addr = r.rm.Ptr(addr) // deref VALUE -> RString object + default: + return "", fmt.Errorf("unexpected pathobj type tag: 0x%X", flags&rubyTMask) + } + + return r.readRubyString(addr) +} + +// readRubyString extracts a Ruby string from the given addr. +// +// 2.5.0: https://github.com/ruby/ruby/blob/4e0a51297/include/ruby/ruby.h#L1004 +// 3.0.0: https://github.com/ruby/ruby/blob/48b94b791/include/ruby/internal/core/rstring.h#L73 +func (r *rubyInstance) readRubyString(addr libpf.Address) (string, error) { + flags := r.rm.Ptr(addr) + if flags&rubyTMask != rubyTString { + return "", fmt.Errorf("object at 0x%08X is not a string", addr) + } + + var str string + vms := &r.r.vmStructs + if flags&rstringNoEmbed == rstringNoEmbed { + str = r.rm.StringPtr(addr + libpf.Address(vms.rstring_struct.as_heap_ptr)) + } else { + str = r.rm.String(addr + libpf.Address(vms.rstring_struct.as_ary)) + } + + r.addrToString.Add(addr, str) + return str, nil +} + +type StringReader = func(address libpf.Address) (string, error) + +// getStringCached retrieves a string from cache or reads and inserts it if it's missing. +func (r *rubyInstance) getStringCached(addr libpf.Address, reader StringReader) (string, error) { + if value, ok := r.addrToString.Get(addr); ok { + return value, nil + } + + str, err := reader(addr) + if err != nil { + return "", err + } + + r.addrToString.Add(addr, str) + return str, err +} + +// rubyPopcount64 is a helper macro. +// Ruby makes use of __builtin_popcount intrinsics. These builtin intrinsics are not available +// here so we use the equivalent function of the Go standard library. +// https://github.com/ruby/ruby/blob/48b94b791997881929c739c64f95ac30f3fd0bb9/internal/bits.h#L408 +func rubyPopcount64(in uint64) uint32 { + return uint32(bits.OnesCount64(in)) +} + +// smallBlockRankGet is a helper macro. +// https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L3432 +func smallBlockRankGet(v uint64, i uint32) uint32 { + if i == 0 { + return 0 + } + return uint32((v >> ((i - 1) * 9))) & 0x1ff +} + +// immBlockRankGet is a helper macro. +// https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L3430 +func immBlockRankGet(v uint64, i uint32) uint32 { + tmp := v >> (i * 7) + return uint32(tmp) & 0x7f +} + +// uint64ToBytes is a helper function to convert an uint64 into its []byte representation. +func uint64ToBytes(val uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, val) + return b +} + +// getObsoleteRubyLineNo implements a binary search algorithm to get the line number for a position. +// +// Implementation according to Ruby: +// https://github.com/ruby/ruby/blob/4e0a512972cdcbfcd5279f1a2a81ba342ed75b6e/iseq.c#L1254-L1295 +func (r *rubyInstance) getObsoleteRubyLineNo(iseqBody libpf.Address, + pos, size uint32) (uint32, error) { + vms := &r.r.vmStructs + sizeOfEntry := uint32(vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry) + + 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") + } + if uint32(len(*syncPoolData)) < size*sizeOfEntry { + // make sure the data we want to write into blob fits in + *syncPoolData = make([]byte, size*sizeOfEntry) + } + defer func() { + // Reset memory and return it for reuse. + for i := uint32(0); i < size*sizeOfEntry; i++ { + (*syncPoolData)[i] = 0x0 + } + r.memPool.Put(syncPoolData) + }() + blob := (*syncPoolData)[:size*sizeOfEntry] + + // Read the table with multiple iseq_insn_info_entry entries only once for the binary search. + if err := r.rm.Read(ptr, blob); err != nil { + return 0, fmt.Errorf("failed to read line table for binary search: %v", err) + } + + var blobPos uint32 + var entryPos, entryLine uint32 + right := size - 1 + left := uint32(1) + + posOffset := uint32(vms.iseq_insn_info_entry.position) + posSize := uint32(vms.iseq_insn_info_entry.size_of_position) + lineNoOffset := uint32(vms.iseq_insn_info_entry.line_no) + lineNoSize := uint32(vms.iseq_insn_info_entry.size_of_line_no) + + for left <= right { + index := left + (right-left)/2 + + blobPos = index * sizeOfEntry + + entryPos = binary.LittleEndian.Uint32( + blob[blobPos+posOffset : blobPos+posOffset+posSize]) + entryLine = binary.LittleEndian.Uint32( + blob[blobPos+lineNoOffset : blobPos+lineNoOffset+lineNoSize]) + + if entryPos == pos { + return entryLine, nil + } + + if entryPos < pos { + left = index + 1 + continue + } + right = index - 1 + } + + if left >= size { + blobPos = (size - 1) * sizeOfEntry + return binary.LittleEndian.Uint32( + blob[blobPos+lineNoOffset : blobPos+lineNoOffset+lineNoSize]), nil + } + + blobPos = left * sizeOfEntry + entryPos = binary.LittleEndian.Uint32(blob[blobPos+posOffset : blobPos+posOffset+posSize]) + + if entryPos > pos { + blobPos = (left - 1) * sizeOfEntry + return binary.LittleEndian.Uint32( + blob[blobPos+lineNoOffset : blobPos+lineNoOffset+lineNoSize]), nil + } + return binary.LittleEndian.Uint32( + blob[blobPos+lineNoOffset : blobPos+lineNoOffset+lineNoSize]), nil +} + +// getRubyLineNo extracts the line number information from the given instruction sequence body and +// Ruby VM program counter. +// Starting with Ruby version 2.6.0 [0] Ruby no longer stores the information about the line number +// in a struct field but encodes them in a succinct data structure [1]. +// For the lookup of the line number in this data structure getRubyLineNo follows the naming and +// implementation of the Ruby internal function succ_index_lookup [2]. +// +// [0] https://github.com/ruby/ruby/commit/83262f24896abeaf1977c8837cbefb1b27040bef +// [1] https://en.wikipedia.org/wiki/Succinct_data_structure +// [2] https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L3500-L3517 +func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, error) { + vms := &r.r.vmStructs + + // Read the struct iseq_constant_body only once. + blob := make([]byte, vms.iseq_constant_body.size_of_iseq_constant_body) + if err := r.rm.Read(iseqBody, blob); err != nil { + return 0, fmt.Errorf("failed to read iseq_constant_body: %v", err) + } + + offsetEncoded := vms.iseq_constant_body.encoded + iseqEncoded := binary.LittleEndian.Uint64(blob[offsetEncoded : offsetEncoded+8]) + + offsetSize := vms.iseq_constant_body.insn_info_size + size := binary.LittleEndian.Uint32(blob[offsetSize : offsetSize+4]) + + // For our better understanding and future improvement we track the maximum value we get for + // size and report it. + libpf.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 { + offsetBody := vms.iseq_constant_body.insn_info_body + lineNo := binary.LittleEndian.Uint32(blob[offsetBody : offsetBody+4]) + return lineNo, nil + } else 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. + // Returning 0 here is not the correct line number at this point. But we let the + // rest of the symbolization process unwind the frame and get the file name. This + // way we can provide partial results. + return 0, nil + } + + // To get the line number iseq_encoded is subtracted from pc. This result also represents the + // size of the current instruction sequence. If the calculated size of the instruction sequence + // is greater than the value in iseq_encoded we don't report this pc to user space. + // + // nolint:lll + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_backtrace.c#L47-L48 + pos := (pc - iseqEncoded) / uint64(vms.size_of_value) + if pos != 0 { + pos-- + } + + // Ruby 2.6 changed the way of storing line numbers with [0]. As we still want to get + // the line number information for older Ruby versions, we have this special + // handling here. + // + // [0] https://github.com/ruby/ruby/commit/83262f24896abeaf1977c8837cbefb1b27040bef + if r.r.version < 0x20600 { + return r.getObsoleteRubyLineNo(iseqBody, uint32(pos), size) + } + + offsetSuccTable := vms.iseq_constant_body.succ_index_table + succIndexTable := binary.LittleEndian.Uint64(blob[offsetSuccTable : offsetSuccTable+8]) + + if succIndexTable == 0 { + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L1686 + return 0, fmt.Errorf("failed to get table with line information") + } + + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L3500-L3517 + var tableIndex uint32 + if pos < uint64(vms.size_of_immediate_table) { + i := int(pos / 9) + j := int(pos % 9) + 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") + } + tableIndex = immBlockRankGet(immPart, uint32(j)) + } else { + blockIndex := uint32((pos - uint64(vms.size_of_immediate_table)) / 512) + blockOffset := libpf.Address(blockIndex * + uint32(vms.succ_index_table_struct.size_of_succ_dict_block)) + + 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") + } + + blockBitIndex := uint32((pos - uint64(vms.size_of_immediate_table)) % 512) + smallBlockIndex := blockBitIndex / 64 + smallBlockOffset := libpf.Address(smallBlockIndex * uint32(vms.size_of_value)) + + smallBlockRanks := r.rm.Uint64(libpf.Address(succIndexTable) + blockOffset + + 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") + } + + smallBlockPopcount := smallBlockRankGet(smallBlockRanks, smallBlockIndex) + + blockBits := r.rm.Uint64(libpf.Address(succIndexTable) + blockOffset + + 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") + } + popCnt := rubyPopcount64((blockBits << (63 - blockBitIndex%64))) + + tableIndex = rank + smallBlockPopcount + popCnt + } + tableIndex-- + + 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") + } + + 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 lineNo, nil +} + +func (r *rubyInstance) Symbolize(symbolReporter reporter.SymbolReporter, + frame *host.Frame, trace *libpf.Trace) error { + if !frame.Type.IsInterpType(libpf.Ruby) { + return interpreter.ErrMismatchInterpreterType + } + vms := &r.r.vmStructs + + sfCounter := successfailurecounter.New(&r.successCount, &r.failCount) + defer sfCounter.DefaultToFailure() + + // From the eBPF Ruby unwinder we receive the address to the instruction sequence body in + // the Files field. + // + // rb_iseq_constant_body + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L311 + iseqBody := libpf.Address(frame.File) + // The Ruby VM program counter that was extracted from the current call frame is embedded in + // the Linenos field. + pc := frame.Lineno + + key := rubyIseqBodyPC{ + addr: iseqBody, + pc: uint64(pc), + } + + if iseq, ok := r.iseqBodyPCToFunction.Get(key); ok { + trace.AppendFrame(libpf.RubyFrame, iseq.fileID, iseq.line) + sfCounter.ReportSuccess() + return nil + } + + lineNo, err := r.getRubyLineNo(iseqBody, uint64(pc)) + if err != nil { + return err + } + + sourceFileNamePtr := r.rm.Ptr(iseqBody + + libpf.Address(vms.iseq_constant_body.location+vms.iseq_location_struct.pathobj)) + sourceFileName, err := r.getStringCached(sourceFileNamePtr, r.readPathObjRealPath) + if err != nil { + return err + } + if !libpf.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", + iseqBody) + } + + funcNamePtr := r.rm.Ptr(iseqBody + + libpf.Address(vms.iseq_constant_body.location+vms.iseq_location_struct.base_label)) + functionName, err := r.getStringCached(funcNamePtr, r.readRubyString) + if err != nil { + return err + } + if !libpf.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", + iseqBody) + } + + pcBytes := uint64ToBytes(uint64(pc)) + iseqBodyBytes := uint64ToBytes(uint64(iseqBody)) + + // 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(functionName)) + _, _ = h.Write(pcBytes) + _, _ = h.Write(iseqBodyBytes) + fileID, err := libpf.FileIDFromBytes(h.Sum(nil)) + if err != nil { + return fmt.Errorf("failed to create a file ID: %v", err) + } + + iseq := &rubyIseq{ + sourceFileName: sourceFileName, + fileID: fileID, + line: libpf.AddressOrLineno(lineNo), + } + r.iseqBodyPCToFunction.Add(key, iseq) + + trace.AppendFrame(libpf.RubyFrame, fileID, libpf.AddressOrLineno(lineNo)) + + // Ruby doesn't provide the information about the function offset for the + // particular line. So we report 0 for this to our backend. + symbolReporter.FrameMetadata( + fileID, + libpf.AddressOrLineno(lineNo), libpf.SourceLineno(lineNo), 0, + functionName, sourceFileName) + + log.Debugf("[%d] [%x] %v+%v at %v:%v", len(trace.FrameTypes), + iseq.fileID, + functionName, 0, + sourceFileName, lineNo) + + sfCounter.ReportSuccess() + return nil +} + +func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + rubyIseqBodyPCStats := r.iseqBodyPCToFunction.GetAndResetStatistics() + addrToStringStats := r.addrToString.GetAndResetStatistics() + + return []metrics.Metric{ + { + ID: metrics.IDRubySymbolizationSuccess, + Value: metrics.MetricValue(r.successCount.Swap(0)), + }, + { + ID: metrics.IDRubySymbolizationFailure, + Value: metrics.MetricValue(r.failCount.Swap(0)), + }, + { + ID: metrics.IDRubyIseqBodyPCHit, + Value: metrics.MetricValue(rubyIseqBodyPCStats.Hit), + }, + { + ID: metrics.IDRubyIseqBodyPCMiss, + Value: metrics.MetricValue(rubyIseqBodyPCStats.Miss), + }, + { + ID: metrics.IDRubyIseqBodyPCAdd, + Value: metrics.MetricValue(rubyIseqBodyPCStats.Added), + }, + { + ID: metrics.IDRubyIseqBodyPCDel, + Value: metrics.MetricValue(rubyIseqBodyPCStats.Deleted), + }, + { + ID: metrics.IDRubyAddrToStringHit, + Value: metrics.MetricValue(addrToStringStats.Hit), + }, + { + ID: metrics.IDRubyAddrToStringMiss, + Value: metrics.MetricValue(addrToStringStats.Miss), + }, + { + ID: metrics.IDRubyAddrToStringAdd, + Value: metrics.MetricValue(addrToStringStats.Added), + }, + { + ID: metrics.IDRubyAddrToStringDel, + Value: metrics.MetricValue(addrToStringStats.Deleted), + }, + { + ID: metrics.IDRubyMaxSize, + Value: metrics.MetricValue(r.maxSize.Swap(0)), + }, + }, nil +} + +// determineRubyVersion looks for the symbol ruby_version and extracts version +// information from its value. +func determineRubyVersion(ef *pfelf.File) (uint32, error) { + sym, err := ef.LookupSymbol("ruby_version") + if err != nil { + return 0, fmt.Errorf("symbol ruby_version not found: %v", err) + } + + memory := make([]byte, 5) + if _, err := ef.ReadVirtualMemory(memory, int64(sym.Address)); err != nil { + return 0, fmt.Errorf("failed to read process memory at 0x%x:%v", + sym.Address, err) + } + + matches := rubyVersionRegex.FindStringSubmatch(string(memory)) + + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + release, _ := strconv.Atoi(matches[3]) + + return uint32(major*0x10000 + minor*0x100 + release), nil +} + +func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + if !rubyRegex.MatchString(info.FileName()) { + return nil, nil + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + version, err := determineRubyVersion(ef) + if err != nil { + return nil, err + } + + // Reason for lowest supported version: + // - Ruby 2.5 is still commonly used at time of writing this code. + // https://www.jetbrains.com/lp/devecosystem-2020/ruby/ + // Reason for maximum supported version 3.2.x: + // - this is currently the newest stable version + + const minVer, maxVer = 0x20500, 0x30300 + 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, + (minVer>>16)&0xff, (minVer>>8)&0xff, minVer&0xff, + (maxVer>>16)&0xff, (maxVer>>8)&0xff, maxVer&0xff) + } + + // Before Ruby 2.5 the symbol ruby_current_thread was used for the current execution + // context but got replaced in [0] with ruby_current_execution_context_ptr. + // With [1] the Ruby internal execution model changed and the symbol + // ruby_current_execution_context_ptr was removed. Therefore we need to lookup different + // symbols depending on the version. + // [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 { + currentCtxSymbol = "ruby_current_execution_context_ptr" + } + currentCtxPtr, err := ef.LookupSymbolAddress(currentCtxSymbol) + if err != nil { + return nil, fmt.Errorf("%v not found: %v", currentCtxSymbol, err) + } + + // rb_vm_exec is used to execute the Ruby frames in the Ruby VM and is called within + // 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 { + symbolName = libpf.SymbolName("ruby_exec_node") + } + interpRanges, err := info.GetSymbolAsRanges(symbolName) + if err != nil { + return nil, err + } + + rid := &rubyData{ + version: version, + currentCtxPtr: libpf.Address(currentCtxPtr), + } + + vms := &rid.vmStructs + + // Ruby does not provide introspection data, hard code the struct field offsets. 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.execution_context_struct.vm_stack = 0 + vms.execution_context_struct.vm_stack_size = 8 + vms.execution_context_struct.cfp = 16 + + vms.control_frame_struct.pc = 0 + vms.control_frame_struct.iseq = 16 + vms.control_frame_struct.ep = 32 + if version < 0x20600 { + vms.control_frame_struct.size_of_control_frame_struct = 48 + } else if version < 0x30100 { + // 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + // The position field was removed from this struct with + // https://github.com/ruby/ruby/commit/295838e6eb1d063c64f7cde5bbbd13c7768908fd + vms.iseq_insn_info_entry.position = 0 + vms.iseq_insn_info_entry.size_of_position = 0 + 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 { + // https://github.com/ruby/ruby/commit/0a36cab1b53646062026c3181117fad73802baf4 + vms.iseq_insn_info_entry.position = 0 + vms.iseq_insn_info_entry.size_of_position = 0 + 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 = 12 + } + + if version < 0x30200 { + vms.rstring_struct.as_ary = 16 + } else { + vms.rstring_struct.as_ary = 24 + } + vms.rstring_struct.as_heap_ptr = 24 + + vms.rarray_struct.as_ary = 16 + vms.rarray_struct.as_heap_ptr = 32 + + vms.succ_index_table_struct.small_block_ranks = 8 + vms.succ_index_table_struct.block_bits = 16 + vms.succ_index_table_struct.succ_part = 48 + vms.succ_index_table_struct.size_of_succ_dict_block = 80 + vms.size_of_immediate_table = 54 + + vms.size_of_value = 8 + + if version >= 0x30000 { + if runtime.GOARCH == "amd64" { + vms.rb_ractor_struct.running_ec = 0x208 + } else { + vms.rb_ractor_struct.running_ec = 0x218 + } + } + + if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindRuby, info.FileID(), + interpRanges); err != nil { + return nil, err + } + + return rid, nil +} diff --git a/interpreter/types.go b/interpreter/types.go new file mode 100644 index 00000000..4a2805eb --- /dev/null +++ b/interpreter/types.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 interpreter + +import ( + "errors" + "unsafe" + + "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/reporter" + "github.com/elastic/otel-profiling-agent/tpbase" +) + +const ( + // LruFunctionCacheSize is the LRU size for caching functions for an interpreter. + // This should reflect the number of hot functions that are seen often in a trace. + LruFunctionCacheSize = 1024 + + // UnknownSourceFile is the source file name to use when the real one is not available + UnknownSourceFile = "" + + // TopLevelFunctionName is the name to be used when a function does not have a name, + // but we can deduce that it is at the highest possible scope (e.g for top-level PHP code) + TopLevelFunctionName = "" +) + +var ( + ErrMismatchInterpreterType = errors.New("mismatched interpreter type") +) + +// The following function Loader and interfaces Data and Instance work together +// as an abstraction to support language specific eBPF unwinding and host agent side symbolization +// of frames. +// +// Functionality for these interfaces is divided as follows: +// +// 1. Loader is responsible for recognizing if the given mapping/ELF DSO matches by name, +// and later by content, to an interpreter supported by the specific implementation. +// If yes, it returns Data for this specific DSO. The Loader loads and checks data +// from given ELF DSO. The intent is to load needed symbols and keep their addresses +// relative to the file virtual address space. It can also load static data from the +// DSO, such as the exact interpreter version string or number. All this is returned +// in a data structure that implements Data interface. +// +// 2. Data is the interface to operate on per-ELF DSO data. ProcessManager receives this +// interface from the Loader, and stores it in a map to cache them by FileID. That is, +// each ELF DSO is probed by the Loaders only the first time it is seen. The returned +// Data is then reused for all other processes using the same ELF DSO without need to +// extract information from it by the loader. +// +// The Attach method will populate the needed eBPF maps with the pre-parsed data from +// the Data and PID specific AttachData. E.g. it can calculate the target memory addresses +// by adding the file virtual address from cached Data and the process AttachData mapping +// "bias". If additional per-PID structures need to be maintained it can instantiate new +// Instance structures for those. Finally an Instance interface is returned: either to +// the per-PID Instance structure, or if no per-PID data is kept, the main Data structure +// can also implement this interface. +// +// 3. Instance is the interface to operate on per-PID data. This interface +// is tracked by the ProcessManager by PID and the mapped-at-address. +// +// The ProcessManager will delegate frame symbolization to this interface, +// and it will also call this interface's Detach to clean up eBPF maps release any +// per-PID resource held. +// +// The split of Data and Instance and the way the methods signatures are designed (passing ebpfMaps +// and pid) allows an interpreter implementation to keep just per-ELF information (when the xxxData +// implements both interfaces) or additionally track per-PID information (separate data types for +// the Data and Instance). +// +// Data (and Instance) should generally match one eBPF tracer implementation. However, it is +// possible to have several Loaders that would return same type of Data. For example, +// xxInterpreter 2 and xxInterpreter 3 can likely be unwound by the same unwinding strategy but +// perhaps the symbol names or the way to extract introspection data is different. Or perhaps we +// 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 + + // UpdateProcData adds the given interpreter data to the named eBPF map. + UpdateProcData(typ libpf.InterpType, pid libpf.PID, data unsafe.Pointer) error + + // DeleteProcData removes any data from the named eBPF map. + DeleteProcData(typ libpf.InterpType, pid libpf.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 + + // DeletePidInterpreterMapping removes the element specified by pid, prefix + // rom the eBPF map pid_page_to_mapping_info. + DeletePidInterpreterMapping(libpf.PID, lpm.Prefix) error +} + +// Loader is a function to detect and load data from given interpreter ELF file. +// ProcessManager will call each configured Loader in order to see if additional handling and data +// is needed to unwind interpreter frames. +// +// A Loader can return one of the following value combinations: +// +// - `nil, nil`, indicating that it didn't detect the interpreter to belong to it +// - `data, nil`, indicating that it wants to handle the executable +// - `nil, error`, indicating that a permanent failure occurred during interpreter detection +type Loader func(ebpf EbpfHandler, info *LoaderInfo) (Data, error) + +// Data is the interface to operate on per-ELF DSO data. +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) ( + Instance, error) +} + +// Instance is the interface to operate on per-PID data. +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 + + // SynchronizeMappings is called when the processmanager has reread process memory + // mappings. Interpreters not needing to process these events can simply ignore them + // by just returning a nil. + SynchronizeMappings(ebpf EbpfHandler, symbolReporter reporter.SymbolReporter, + pr process.Process, mappings []process.Mapping) error + + // 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 + + // 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. + Symbolize(symbolReporter reporter.SymbolReporter, frame *host.Frame, + trace *libpf.Trace) error + + // GetAndResetMetrics collects the metrics from the Instance and resets + // the counters to their initial value. + GetAndResetMetrics() ([]metrics.Metric, error) +} diff --git a/legal/append-non-go-info.sh b/legal/append-non-go-info.sh new file mode 100755 index 00000000..8d0b574c --- /dev/null +++ b/legal/append-non-go-info.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Script to append legal information for non Go dependencies. + +set -eu +set -o pipefail + +nonGoDependencies="${1:-non-go-dependencies.json}" +depsFile="${2:-deps.csv}" + +for item in $(jq -c . "${nonGoDependencies}"); do + dependency=$(jq -r '.Dependency' <<< "$item") + version=$(jq -r '.Version' <<< "$item") + licence=$(jq -r '.Licence' <<< "$item") + url=$(jq -r '.URL' <<< "$item") + { + echo "$dependency,$url,$version,,$licence" + } >> "${depsFile}" +done diff --git a/legal/non-go-dependencies.json b/legal/non-go-dependencies.json new file mode 100644 index 00000000..79ef5104 --- /dev/null +++ b/legal/non-go-dependencies.json @@ -0,0 +1,7 @@ +{ + "Dependency": "zyantific/zydis", + "Version": "v3.1.0", + "Licence": "MIT", + "URL": "https://zydis.re", + "LicenceFile": "https://raw.githubusercontent.com/zyantific/zydis/v3.1.0/LICENSE" +} diff --git a/legal/rules.json b/legal/rules.json new file mode 100644 index 00000000..91db928c --- /dev/null +++ b/legal/rules.json @@ -0,0 +1,10 @@ +{ + "allowlist": [ + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MIT", + "MPL-2.0" + ] +} diff --git a/legal/templates/deps.csv.tmpl b/legal/templates/deps.csv.tmpl new file mode 100644 index 00000000..fc21fd82 --- /dev/null +++ b/legal/templates/deps.csv.tmpl @@ -0,0 +1,7 @@ +{{- define "depInfo" -}} +{{- range $i, $dep := . }} +{{ $dep.Name }},{{ $dep.URL }},{{ $dep.Version | canonicalVersion }},{{ $dep.Version | revision }},{{ $dep.LicenceType }} +{{- end -}} +{{- end -}} + +name,url,version,revision,license,sourceURL{{ template "depInfo" .Direct }}{{ template "depInfo" .Indirect }} diff --git a/libpf/armhelpers/arm_helpers.go b/libpf/armhelpers/arm_helpers.go new file mode 100644 index 00000000..f133d24d --- /dev/null +++ b/libpf/armhelpers/arm_helpers.go @@ -0,0 +1,176 @@ +/* + * 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. + */ + +// This package contains a series of helper functions that are useful for ARM disassembly. +package armhelpers + +import ( + "fmt" + "strconv" + "strings" + + "github.com/elastic/otel-profiling-agent/libpf/stringutil" + aa "golang.org/x/arch/arm64/arm64asm" +) + +// Xreg2num converts arm64asm Reg or RegSP X0...X30 and W0...W30 register enum into a register +// number. X0/W0 return 0, X1/W1 return 1, etc. +func Xreg2num(arg interface{}) (int, bool) { + var ndx aa.Reg + switch reg := arg.(type) { + case aa.Reg: + ndx = reg + case aa.RegSP: + ndx = aa.Reg(reg) + case aa.RegExtshiftAmount: + // Similar to other instructions, fields of RegExtshiftAmount are not exported. + // https://github.com/golang/go/issues/51517 + n, ok := DecodeRegister(arg.(aa.RegExtshiftAmount).String()) + if !ok { + return 0, false + } + ndx = aa.Reg(n) + default: + return 0, false + } + + switch { + case ndx >= aa.X0 && ndx <= aa.X30: + return int(ndx - aa.X0), true + case ndx >= aa.W0 && ndx <= aa.W30: + return int(ndx - aa.W0), true + } + + return 0, false +} + +// DecodeRegister converts the result of calling Reg.String() +// into the initial register's value. +func DecodeRegister(reg string) (uint64, bool) { + // This function is essentially just the inverse + // of https://cs.opensource.google/go/x/arch/+/fc48f9fe:arm64/arm64asm/inst.go;l=335 + length := len(reg) + if length == 0 { + return 0, false + } + + // WZR and XZR don't have a value. + if reg == "WZR" || reg == "XZR" { + return 0, false + } + + // The special case is having a string containing Reg(%d). + if length > 3 && reg[0:2] == "Reg" { + val, err := strconv.ParseUint(reg[3:length-1], 10, 64) + if err != nil { + return 0, false + } + return val, true + } + + // Otherwise, we want to strip out the + // leading character only if the + // character is one of a few. + var regOffset uint64 + switch reg[0] { + case 'W': + regOffset = uint64(aa.W0) + case 'X': + regOffset = uint64(aa.X0) + case 'B': + regOffset = uint64(aa.B0) + case 'H': + regOffset = uint64(aa.H0) + case 'S': + regOffset = uint64(aa.S0) + case 'D': + regOffset = uint64(aa.D0) + case 'Q': + regOffset = uint64(aa.Q0) + case 'V': + regOffset = uint64(aa.V0) + default: + return 0, false + } + + val, err := strconv.ParseUint(reg[1:], 10, 64) + if err != nil { + return 0, false + } + + return val + regOffset, true +} + +// DecodeImmediate converts an arm64asm Arg of immediate type to it's value. +func DecodeImmediate(arg aa.Arg) (uint64, bool) { + switch val := arg.(type) { + case aa.Imm: + return uint64(val.Imm), true + case aa.PCRel: + return uint64(val), true + case aa.MemImmediate: + // The MemImmediate layout changes quite + // a bit depending on its mode. + var fields [2]string + // All of the strings we are formatted in the following way: + // 1) They all (except 1) contain a comma + // 2) They all (except 1) have the offset as the second parameter. + // The exception is aa.AddrOffset when the offset is 0. + n := stringutil.SplitN(val.String(), ",", fields[:]) + if n == 0 || n > 2 { + // This is a failure case + return 0, false + } + + if n == 1 { + // This should happen only if we have an AddrOffset where there's + // a 0 offset. See + // https://cs.opensource.google/go/x/arch/+/fc48f9fe:arm64/arm64asm/inst.go;l=515 + return 0, true + } + + // In all other cases we want to split the string around the comma and + // extract the second number. Note that the string will start with a # + // in all but one case (namely, AddrPostReg). + pos := strings.Index(fields[1], "#") + if pos == -1 { + // We have a string that looks like this: + // [%s], %s + // Note that the second %s here is the print + // format from a register. Annoyingly this isn't a + // register type, so we have to unwind it manually + val, err := DecodeRegister(fields[1]) + if !err { + return 0, false + } + // The Go disassembler always adds X0 here. + // See https://cs.opensource.google/go/x/arch/+/fc48f9fe:arm64/arm64asm/inst.go;l=526 + return val - uint64(aa.X0), true + } + + // Otherwise all of the strings end with a ], so we just parse + // the string before that. + endIndex := strings.Index(fields[1], "]") + // The strings are base 10 encoded + out, err := strconv.ParseInt(fields[1][pos+1:endIndex], 10, 64) + if err != nil { + return 0, false + } + return uint64(out), true + + case aa.ImmShift: + // Sadly, ImmShift{} does not have public fields. + // https://github.com/golang/go/issues/51517 + var imm uint64 + n, err := fmt.Sscanf(val.String(), "#%v", &imm) + if err != nil || n != 1 { + return 0, false + } + return imm, true + } + + return 0, false +} diff --git a/libpf/basehash/basehash.go b/libpf/basehash/basehash.go new file mode 100644 index 00000000..ca0d1381 --- /dev/null +++ b/libpf/basehash/basehash.go @@ -0,0 +1,101 @@ +/* + * 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 basehash provides basic types to implement hash identifiers. +package basehash + +import ( + "fmt" + "strconv" + "strings" +) + +// In a variety of different places the profiling agent identifies files or traces with +// an identifier. This identifier consists of two 64-bit integers on the database +// layer, but is really a 128-bit hash. +// +// At the moment, most of the eBPF infrastructure that the profiling agent contains still +// uses 64-bit hashes. +// +// In order to have a good migration path for 64-bit hashes to 128-bit hashes, +// the code defines a "base type" called baseHash of 64 bits here, along with +// various methods for marshaling/unmarshaling to JSON and regular string here. +// +// Hopefully, when the rest of the code is ready, upgrading the types here to +// a struct that contains two uint64s should be easy. + +const lowerHex = "0123456789abcdef" +const upperHex = "0123456789ABCDEF" + +func putUint64AsHex(n uint64, b []byte, mapping string) { + b[0] = mapping[(n>>60)&0x0F] + b[1] = mapping[(n>>56)&0x0F] + b[2] = mapping[(n>>52)&0x0F] + b[3] = mapping[(n>>48)&0x0F] + b[4] = mapping[(n>>44)&0x0F] + b[5] = mapping[(n>>40)&0x0F] + b[6] = mapping[(n>>36)&0x0F] + b[7] = mapping[(n>>32)&0x0F] + b[8] = mapping[(n>>28)&0x0F] + b[9] = mapping[(n>>24)&0x0F] + b[10] = mapping[(n>>20)&0x0F] + b[11] = mapping[(n>>16)&0x0F] + b[12] = mapping[(n>>12)&0x0F] + b[13] = mapping[(n>>8)&0x0F] + b[14] = mapping[(n>>4)&0x0F] + b[15] = mapping[n&0x0F] +} + +// putUint64AsLowerHex encodes a uint64 into b as a lowercase hexadecimal. +func putUint64AsLowerHex(n uint64, b []byte) { + putUint64AsHex(n, b, lowerHex) +} + +// putUint64AsUpperHex encodes a uint64 into b as an uppercase hexadecimal. +func putUint64AsUpperHex(n uint64, b []byte) { + putUint64AsHex(n, b, upperHex) +} + +func uint64ToString(n uint64) string { + return strconv.FormatUint(n, 10) +} + +func uint64ToGob(n uint64, ch rune) string { + return fmt.Sprintf("%%!%c(uint64=%s)", ch, uint64ToString(n)) +} + +func uint64ToLowerHex(n uint64) string { + return strconv.FormatUint(n, 16) +} + +func uint64ToUpperHex(n uint64) string { + return strings.ToUpper(strconv.FormatUint(n, 16)) +} + +// These marshaling helper methods assist the Go compiler with inlining while +// still improving readability. +func marshalIdentifierTo(hi, lo uint64, b []byte) { + putUint64AsLowerHex(hi, b[0:16]) + putUint64AsLowerHex(lo, b[16:32]) +} + +func marshalQuotedIdentifierTo(hi, lo uint64, b []byte) { + b[0] = '"' + marshalIdentifierTo(hi, lo, b[1:]) + b[33] = '"' +} + +func marshalIdentifier(hi, lo uint64) []byte { + buf := make([]byte, 32) + marshalIdentifierTo(hi, lo, buf) + return buf +} + +func marshalQuotedIdentifier(hi, lo uint64) []byte { + buf := make([]byte, 34) + marshalQuotedIdentifierTo(hi, lo, buf) + return buf +} diff --git a/libpf/basehash/hash128.go b/libpf/basehash/hash128.go new file mode 100644 index 00000000..0e722755 --- /dev/null +++ b/libpf/basehash/hash128.go @@ -0,0 +1,248 @@ +/* + * 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 basehash + +import ( + "encoding" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/google/uuid" +) + +// Hash128 represents a uint128 using two uint64s. +// +// hi represents the most significant 64 bits and lo represents the least +// significant 64 bits. +type Hash128 struct { + hi uint64 + lo uint64 +} + +func (h Hash128) ToUUIDString() string { + // The following can't fail: we are guaranteed to get a slice of the correct length. + id, _ := uuid.FromBytes(h.Bytes()) + return id.String() +} + +// Base64 returns the base64 FileID representation for inclusion into JSON payloads. +func (h Hash128) Base64() string { + return base64.RawURLEncoding.EncodeToString(h.Bytes()) +} + +func New128(hi, lo uint64) Hash128 { + return Hash128{hi, lo} +} + +// New128FromBytes returns a Hash128 given by the bytes in b. +func New128FromBytes(b []byte) (Hash128, error) { + if len(b) != 16 { + return Hash128{}, fmt.Errorf("invalid length for bytes: %d", len(b)) + } + h := Hash128{} + h.hi = binary.BigEndian.Uint64(b[0:8]) + h.lo = binary.BigEndian.Uint64(b[8:16]) + return h, nil +} + +// New128FromString returns a Hash128 given by the characters in s. +func New128FromString(s string) (Hash128, error) { + // The Hash128 Format prefixes the string with "0x" for some given formatter. + s = strings.TrimPrefix(s, "0x") + // In the UUID representation the hash string contains "-". + s = strings.ReplaceAll(s, "-", "") + if len(s) != 32 { + return Hash128{}, fmt.Errorf("invalid length for string '%s': %d", s, len(s)) + } + hi, err := strconv.ParseUint(s[0:16], 16, 64) + if err != nil { + return Hash128{}, err + } + lo, err := strconv.ParseUint(s[16:32], 16, 64) + if err != nil { + return Hash128{}, err + } + return New128(hi, lo), nil +} + +// Less reports whether h is less than other. +// +// The order defined here must be the same as the one used in "SELECT ... FOR UPDATE" queries, +// otherwise DB deadlocks may occur on concurrent updates, hence the casts to int64. +func (h Hash128) Less(other Hash128) bool { + return int64(h.hi) < int64(other.hi) || + (h.hi == other.hi && int64(h.lo) < int64(other.lo)) +} + +func (h Hash128) Equal(other Hash128) bool { + return h.hi == other.hi && h.lo == other.lo +} + +func (h Hash128) IsZero() bool { + return h.hi == 0 && h.lo == 0 +} + +// Compare returns an integer comparing two hashes lexicographically. +// The result will be 0 if h == other, -1 if h < other, and +1 if h > other. +func (h Hash128) Compare(other Hash128) int { + if int64(h.hi) < int64(other.hi) { + return -1 + } + if int64(h.hi) > int64(other.hi) { + return 1 + } + if int64(h.lo) < int64(other.lo) { + return -1 + } + if int64(h.lo) > int64(other.lo) { + return 1 + } + return 0 +} + +// copyBytes copies the byte slice representation of a Hash128 into b. +func (h Hash128) copyBytes(b []byte) []byte { + binary.BigEndian.PutUint64(b[0:8], h.hi) + binary.BigEndian.PutUint64(b[8:16], h.lo) + return b +} + +// Bytes returns a byte slice representation of a Hash128. +func (h Hash128) Bytes() []byte { + return h.copyBytes(make([]byte, 16)) +} + +// Format implements fmt.Formatter. +// +// It accepts the formats 'd' (decimal), 'v' (value), 'x' +// (lowercase hexadecimal), and 'X' (uppercase hexadecimal). +// +// Also supported is a subset of the package fmt's format +// flags, including '#' for leading zero in hexadecimal. +// +// For any unsupported format, the value will be serialized +// using the gob codec. +func (h Hash128) Format(s fmt.State, ch rune) { + if s.Flag('#') { + if ch == 'x' || ch == 'v' { + s.Write([]byte("0x")) + s.Write([]byte(uint64ToLowerHex(h.hi))) + buf := make([]byte, 16) + putUint64AsLowerHex(h.lo, buf) + s.Write(buf) + return + } + + if ch == 'X' { + s.Write([]byte("0x")) + s.Write([]byte(uint64ToUpperHex(h.hi))) + buf := make([]byte, 16) + putUint64AsUpperHex(h.lo, buf) + s.Write(buf) + return + } + } + + if ch == 'x' { + s.Write([]byte(uint64ToLowerHex(h.hi))) + buf := make([]byte, 16) + putUint64AsLowerHex(h.lo, buf) + s.Write(buf) + return + } + + if ch == 'X' { + s.Write([]byte(uint64ToUpperHex(h.hi))) + buf := make([]byte, 16) + putUint64AsUpperHex(h.lo, buf) + s.Write(buf) + return + } + + if ch == 'd' || ch == 'v' { + fmt.Fprintf(s, "{%d %d}", h.hi, h.lo) + return + } + + fmt.Fprintf(s, "{%s %s}", uint64ToGob(h.hi, ch), uint64ToGob(h.lo, ch)) +} + +func (h Hash128) StringNoQuotes() string { + return string(marshalIdentifier(h.hi, h.lo)) +} + +func (h Hash128) Words() (hi, lo uint64) { + return h.hi, h.lo +} + +func (h Hash128) MarshalJSON() ([]byte, error) { + return marshalQuotedIdentifier(h.hi, h.lo), nil +} + +func (h *Hash128) UnmarshalJSON(b []byte) error { + if len(b) != 34 { + return fmt.Errorf("invalid length for bytes: %d", len(b)) + } + hash128, err := New128FromString(string(b)[1:33]) + if err != nil { + return err + } + h.hi = hash128.hi + h.lo = hash128.lo + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface, so we can +// marshal (from JSON) a map using a Hash128 as a key +func (h Hash128) MarshalText() ([]byte, error) { + // Implements the encoding.TextMarshaler interface, so we can + // marshal (from JSON) a map using a Hash128 as a key + return marshalIdentifier(h.hi, h.lo), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface, so we can +// unmarshal (from JSON) a map using a Hash128 as a key +func (h *Hash128) UnmarshalText(text []byte) error { + // Implements the encoding.TextUnmarshaler interface, so we can + // unmarshal (from JSON) a map using a Hash128 as a key + hash128, err := New128FromString(string(text)) + if err != nil { + return err + } + h.hi = hash128.hi + h.lo = hash128.lo + return nil +} + +// Hi returns the high 64 bits +func (h Hash128) Hi() uint64 { + return h.hi +} + +// Lo returns the low 64 bits +func (h Hash128) Lo() uint64 { + return h.lo +} + +// PutBytes16 writes the 16 bytes into the provided array pointer. +func (h Hash128) PutBytes16(b *[16]byte) { + // The following can't fail since the length is 16 bytes + _ = h.copyBytes(b[0:16]) +} + +// Compile-time interface checks +var _ fmt.Formatter = (*Hash128)(nil) + +var _ encoding.TextUnmarshaler = (*Hash128)(nil) +var _ encoding.TextMarshaler = (*Hash128)(nil) + +var _ json.Marshaler = (*Hash128)(nil) +var _ json.Unmarshaler = (*Hash128)(nil) diff --git a/libpf/basehash/hash128_test.go b/libpf/basehash/hash128_test.go new file mode 100644 index 00000000..82bab411 --- /dev/null +++ b/libpf/basehash/hash128_test.go @@ -0,0 +1,179 @@ +/* + * 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 basehash + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFromBytes(t *testing.T) { + _, err := New128FromBytes(nil) + assert.Error(t, err) + + b := []byte{} + _, err = New128FromBytes(b) + assert.Error(t, err) + + b = []byte{1} + _, err = New128FromBytes(b) + assert.Error(t, err) + + b = []byte{0, 1, 2, 3, 4, 5, 6, 7} + _, err = New128FromBytes(b) + assert.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) + assert.Equal(t, New128(0x01020304050607, 0x08090A0B0C0D0E0F), hash) +} + +func TestEqual(t *testing.T) { + hash := New128(0xDEC0DE, 0xC0FFEE) + + assert.True(t, hash.Equal(New128(0xDEC0DE, 0xC0FFEE))) + assert.False(t, hash.Equal(New128(0xDEC0DE, 0))) + assert.False(t, hash.Equal(New128(0, 0xC0FFEE))) + assert.False(t, hash.Equal(New128(0xDECADE, 0xCAFE))) +} + +func TestLess(t *testing.T) { + // left.hi == right.hi and left.lo < right.lo + a, b := New128(0, 1), New128(0, 2) + assert.True(t, a.Less(b)) + + // left.hi == right.hi and left.lo > right.lo + c, d := New128(0, 2), New128(0, 1) + assert.False(t, c.Less(d)) + + // left.hi == right.hi and left.lo == right.lo + e, f := New128(0, 2), New128(0, 2) + assert.False(t, e.Less(f)) + + // left.hi < right.hi + g, h := New128(0, 0), New128(1, 1) + assert.True(t, g.Less(h)) + + // left.hi > right.hi + i, j := New128(1, 1), New128(0, 0) + assert.False(t, i.Less(j)) +} + +func TestIsZero(t *testing.T) { + assert.True(t, New128(0, 0).IsZero()) + assert.False(t, New128(5550100, 0).IsZero()) + assert.False(t, New128(0, 5550100).IsZero()) + assert.False(t, New128(5550100, 5550100).IsZero()) +} + +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}) +} + +func TestPutBytes16(t *testing.T) { + var b [16]byte + hash := New128(0x0011223344556677, 0x8899AABBCCDDEEFF) + hash.PutBytes16(&b) + + assert.Equal(t, hash.Bytes(), []byte{ + 0x00, 0x11, 0x22, 0x33, + 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, + 0xCC, 0xDD, 0xEE, 0xFF}) +} + +func TestHash128Format(t *testing.T) { + h, _ := New128FromBytes([]byte{ + 0x00, 0x11, 0x22, 0x33, + 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, + 0xCC, 0xDD, 0xEE, 0xFF}) + + tests := map[string]struct { + formater string + expected string + }{ + "v": {formater: "%v", expected: "{4822678189205111 9843086184167632639}"}, + "d": {formater: "%d", expected: "{4822678189205111 9843086184167632639}"}, + "x": {formater: "%x", expected: "112233445566778899aabbccddeeff"}, + "X": {formater: "%X", expected: "112233445566778899AABBCCDDEEFF"}, + "#v": {formater: "%#v", expected: "0x112233445566778899aabbccddeeff"}, + "#x": {formater: "%#x", expected: "0x112233445566778899aabbccddeeff"}, + "#X": {formater: "%#X", expected: "0x112233445566778899AABBCCDDEEFF"}, + } + + for name, test := range tests { + name := name + 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) + } + }) + } +} + +func TestHash128StringNoQuotes(t *testing.T) { + id := New128(0x0011223344556677, 0x8899AABBCCDDEEFF) + assert.Equal(t, "00112233445566778899aabbccddeeff", id.StringNoQuotes()) +} + +func TestNew128FromString(t *testing.T) { + tests := map[string]struct { //nolint + stringRepresentation string + expected Hash128 + err error + }{ + "hex": {stringRepresentation: "97b7371e9fc83bc7b9ab5ee193a98020", + expected: Hash128{hi: 10932267225134414791, lo: 13378891440972202016}}, + "hex with prefix": {stringRepresentation: "0x97b7371e9fc83bc7b9ab5ee193a98020", + expected: Hash128{hi: 10932267225134414791, lo: 13378891440972202016}}, + "uuid": {stringRepresentation: "97b7371e-9fc8-3bc7-b9ab-5ee193a98020", + expected: Hash128{hi: 10932267225134414791, lo: 13378891440972202016}}, + } + + for name, tc := range tests { + name := name + 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) + } + }) + } +} diff --git a/libpf/basehash/hash64.go b/libpf/basehash/hash64.go new file mode 100644 index 00000000..11ae925b --- /dev/null +++ b/libpf/basehash/hash64.go @@ -0,0 +1,35 @@ +/* + * 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 basehash + +import ( + "encoding/json" + "strconv" +) + +type Hash64 uint64 + +func (h *Hash64) String() string { + return string(marshalQuotedIdentifier(uint64(*h), uint64(*h))) +} + +func (h *Hash64) MarshalJSON() ([]byte, error) { + return marshalQuotedIdentifier(uint64(*h), uint64(*h)), nil +} + +func (h *Hash64) UnmarshalJSON(b []byte) error { + tempHash, err := strconv.ParseUint(string(b)[1:17], 16, 64) + if err != nil { + return err + } + *h = Hash64(tempHash) + return nil +} + +// Compile-time interface checks +var _ json.Marshaler = (*Hash64)(nil) +var _ json.Unmarshaler = (*Hash64)(nil) diff --git a/libpf/basehash/hash64_test.go b/libpf/basehash/hash64_test.go new file mode 100644 index 00000000..167265ab --- /dev/null +++ b/libpf/basehash/hash64_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 basehash + +import ( + "fmt" + "testing" +) + +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) + } + + // Test (Un)MarshalJSON + if data, err = origHash.MarshalJSON(); err != nil { + t.Fatalf("Failed to marshal baseHash64: %v", 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) + } +} diff --git a/libpf/convenience.go b/libpf/convenience.go new file mode 100644 index 00000000..a76d254f --- /dev/null +++ b/libpf/convenience.go @@ -0,0 +1,234 @@ +/* + * 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 ( + "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. +func WriteTempFile(data []byte, directory, prefix string) (string, error) { + file, err := os.CreateTemp(directory, prefix) + if err != nil { + return "", err + } + defer file.Close() + if _, err := file.Write(data); err != nil { + return "", fmt.Errorf("failed to write data to temporary file: %w", err) + } + if err := file.Sync(); err != nil { + return "", fmt.Errorf("failed to synchronize file data: %w", err) + } + return file.Name(), nil +} + +// SleepWithJitter sleeps for baseDuration +/- jitter (jitter is [0..1]) +func SleepWithJitter(baseDuration time.Duration, jitter float64) { + time.Sleep(AddJitter(baseDuration, jitter)) +} + +// SleepWithJitterAndContext blocks for duration +/- jitter (jitter is [0..1]) or until ctx +// is canceled. +func SleepWithJitterAndContext(ctx context.Context, duration time.Duration, jitter float64) error { + tick := time.NewTicker(AddJitter(duration, jitter)) + defer tick.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-tick.C: + return nil + } +} + +// AddJitter adds +/- jitter (jitter is [0..1]) to baseDuration +func AddJitter(baseDuration time.Duration, jitter float64) time.Duration { + if jitter < 0.0 || jitter > 1.0 { + log.Errorf("Jitter (%f) out of range [0..1].", jitter) + return baseDuration + } + // nolint:gosec + 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 + val := reflect.ValueOf(data) + switch val.Kind() { + case reflect.Slice: + if val.Len() != 0 { + e := val.Index(0) + addr := e.Addr().UnsafePointer() + l := val.Len() * int(e.Type().Size()) + s = unsafe.Slice((*byte)(addr), l) + } + case reflect.Ptr: + e := val.Elem() + addr := e.Addr().UnsafePointer() + l := int(e.Type().Size()) + s = unsafe.Slice((*byte)(addr), l) + default: + panic("invalid type") + } + 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 new file mode 100644 index 00000000..70d3a0be --- /dev/null +++ b/libpf/convenience_test.go @@ -0,0 +1,91 @@ +/* + * 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" +) + +func TestMin(t *testing.T) { + a := 3 + b := 2 + if c := min(a, b); c != b { + t.Fatalf("Failed to return expected minimum.") + } +} + +func TestHexTo(t *testing.T) { + tests := map[string]struct { + result uint64 + }{ + "0": {result: 0}, + "FFFFFF": {result: 16777215}, + "42": {result: 66}, + } + + for name, testcase := range tests { + 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) + } + }) + } +} + +func TestDecTo(t *testing.T) { + tests := map[string]struct { + result uint64 + }{ + "0": {result: 0}, + "123": {result: 123}, + "42": {result: 42}, + } + + for name, testcase := range tests { + 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) + } + }) + } +} + +func TestIsValidString(t *testing.T) { + tests := map[string]struct { + input []byte + expected bool + }{ + "empty": {input: []byte{}, expected: false}, + "control sequences": {input: []byte{0x0, 0x1, 0x2, 0x3}, expected: false}, + "record separator": {input: []byte{0x1E}, expected: false}, + "leading NULL": {input: []byte{0x00, 'h', 'e', 'l', 'l', 'o'}, expected: false}, + "leading whitespace": {input: []byte{'\t', 'h', 'e', 'l', 'l', 'o'}, expected: false}, + "trailing whitespace": {input: []byte{'h', 'e', 'l', 'l', 'o', '\t'}, expected: false}, + "middle whitespace": {input: []byte{'h', 'e', 'l', '\t', 'l', 'o'}, expected: false}, + "single word": {input: []byte{'h', 'e', 'l', 'l', 'o'}, expected: true}, + "0xFF": {input: []byte{0xFF}, expected: false}, + "path": {input: []byte("/lib/foo/bar.so@64:123!"), expected: true}, + "日本語": {input: []byte("日本語"), expected: true}, + "emoji": {input: []byte{0xF0, 0x9F, 0x98, 0x8E}, expected: true}, + "invalid UTF-8 sequence 1": {input: []byte{0xE0, 0x76, 0x90}, expected: false}, + "invalid UTF-8 sequence 2": {input: []byte{0x80, 0x8F, 0x75}, expected: false}, + } + + for name, testcase := range tests { + 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) + } + }) + } +} diff --git a/libpf/frameid.go b/libpf/frameid.go new file mode 100644 index 00000000..2b36c387 --- /dev/null +++ b/libpf/frameid.go @@ -0,0 +1,125 @@ +/* + * 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/base64" + "encoding/binary" + "fmt" + "net" + + "github.com/zeebo/xxh3" +) + +// FrameID represents a frame as an address in an executable file +// or as a line in a source code file. +type FrameID struct { + // fileID is the fileID of the frame + fileID FileID + + // addressOrLineno is the address or lineno of the frame + addressOrLineno AddressOrLineno +} + +// NewFrameID creates a new FrameID from the fileId and address or line. +func NewFrameID(fileID FileID, addressOrLineno AddressOrLineno) FrameID { + return FrameID{ + fileID: fileID, + addressOrLineno: addressOrLineno, + } +} + +// NewFrameIDFromString creates a new FrameID from its JSON string representation. +func NewFrameIDFromString(frameEncoded string) (FrameID, error) { + var frameID FrameID + + bytes, err := base64.RawURLEncoding.DecodeString(frameEncoded) + if err != nil { + return frameID, fmt.Errorf("failed to decode frameID %v: %v", frameEncoded, err) + } + + return NewFrameIDFromBytes(bytes) +} + +// NewFrameIDFromBytes creates a new FrameID from a byte array of length 24. +func NewFrameIDFromBytes(bytes []byte) (FrameID, error) { + var frameID FrameID + var err error + + if len(bytes) != 24 { + return frameID, fmt.Errorf("unexpected frameID size (expected 24 bytes): %d", + len(bytes)) + } + + if frameID.fileID, err = FileIDFromBytes(bytes[0:16]); err != nil { + return frameID, fmt.Errorf("failed to create fileID from bytes: %v", err) + } + + frameID.addressOrLineno = AddressOrLineno(binary.BigEndian.Uint64(bytes[16:24])) + + return frameID, nil +} + +// Hash32 returns a 32 bits hash of the input. +// It's main purpose is to be used as key for caching. +func (f FrameID) Hash32() uint32 { + return uint32(f.Hash()) +} + +// String returns the base64 encoded representation. +func (f FrameID) String() string { + return base64.RawURLEncoding.EncodeToString(f.Bytes()) +} + +// EncodeTo encodes the frame ID 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 (f FrameID) EncodeTo(dst []byte) { + base64.RawURLEncoding.Encode(dst, f.Bytes()) +} + +// EncodedLen returns the length of the FrameID's base64 representation. +func (FrameID) EncodedLen() int { + // FrameID is 24 bytes long, the base64 representation is one base64 byte per 6 bits. + return ((16 + 8) * 8) / 6 +} + +// Bytes returns the frameid as byte sequence. +func (f FrameID) Bytes() []byte { + // Using frameID := make([byte, 24]) here makes the function ~5% slower. + var frameID [24]byte + + copy(frameID[:], f.fileID.Bytes()) + binary.BigEndian.PutUint64(frameID[16:], uint64(f.addressOrLineno)) + return frameID[:] +} + +// Hash calculates a hash from the frameid. +// xxh3 is 4x faster than fnv. +func (f FrameID) Hash() uint64 { + return xxh3.Hash(f.Bytes()) +} + +// FileID returns the fileID part of the frameID. +func (f FrameID) FileID() FileID { + return f.fileID +} + +// AddressOrLine returns the addressOrLine part of the frameID. +func (f FrameID) AddressOrLine() AddressOrLineno { + return f.addressOrLineno +} + +// AsIP returns the FrameID as a net.IP type to be used +// for the PC range in profiling-symbols-*. +func (f *FrameID) AsIP() net.IP { + bytes := f.Bytes() + ip := make([]byte, 16) + copy(ip[:8], bytes[:8]) // first 64bits of FileID + copy(ip[8:], bytes[16:]) // addressOrLine + return ip +} diff --git a/libpf/frameid_test.go b/libpf/frameid_test.go new file mode 100644 index 00000000..eda33939 --- /dev/null +++ b/libpf/frameid_test.go @@ -0,0 +1,65 @@ +/* + * 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/assert" +) + +const ( + fileIDLo = 0x77efa716a912a492 + fileIDHi = 0x17445787329fd29a + addressOrLine = 0xe51c +) + +func TestFrameID(t *testing.T) { + var fileID = NewFileID(fileIDLo, fileIDHi) + + tests := []struct { + name string + input string + expected FrameID + bytes []byte + err error + }{ + { + name: "frame base64", + input: "d--nFqkSpJIXRFeHMp_SmgAAAAAAAOUc", + expected: NewFrameID(fileID, addressOrLine), + bytes: []byte{ + 0x77, 0xef, 0xa7, 0x16, 0xa9, 0x12, 0xa4, 0x92, 0x17, 0x44, 0x57, 0x87, + 0x32, 0x9f, 0xd2, 0x9a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x1c, + }, + err: nil, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + frameID, err := NewFrameIDFromString(test.input) + assert.Equal(t, test.err, err) + assert.Equal(t, test.expected, frameID) + + // check if the roundtrip back to the input works + assert.Equal(t, test.input, frameID.String()) + + assert.Equal(t, test.bytes, frameID.Bytes()) + + frameID, err = NewFrameIDFromBytes(frameID.Bytes()) + assert.Equal(t, test.err, err) + assert.Equal(t, test.expected, frameID) + + ip := []byte(frameID.AsIP()) + bytes := frameID.Bytes() + assert.Equal(t, bytes[:8], ip[:8]) + assert.Equal(t, bytes[16:], ip[8:]) + }) + } +} diff --git a/libpf/freelru/lru.go b/libpf/freelru/lru.go new file mode 100644 index 00000000..1356a219 --- /dev/null +++ b/libpf/freelru/lru.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 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/generics.go b/libpf/generics.go new file mode 100644 index 00000000..612dbff5 --- /dev/null +++ b/libpf/generics.go @@ -0,0 +1,79 @@ +/* + * 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 + +// Set is a convenience alias for a map with a `Void` key. +type Set[T comparable] map[T]Void + +// ToSlice converts the Set keys into a slice. +func (s Set[T]) ToSlice() []T { + slice := make([]T, 0, len(s)) + for item := range s { + slice = append(slice, item) + } + return slice +} + +// MapKeysToSlice creates a slice from a map's keys. +func MapKeysToSlice[K comparable, V any](m map[K]V) []K { + slice := make([]K, 0, len(m)) + for key := range m { + slice = append(slice, key) + } + return slice +} + +// MapValuesToSlice creates a slice from a map's values. +func MapValuesToSlice[K comparable, V any](m map[K]V) []V { + slice := make([]V, 0, len(m)) + for _, value := range m { + slice = append(slice, value) + } + return slice +} + +// SliceToSet creates a set from a slice, deduplicating it. +func SliceToSet[T comparable](s []T) Set[T] { + set := make(map[T]Void, len(s)) + for _, item := range s { + set[item] = Void{} + } + return set +} + +// SliceAllEqual checks whether all items in a slice have a given value. +func SliceAllEqual[T comparable](s []T, value T) bool { + for _, item := range s { + if item != value { + return false + } + } + + return true +} + +// SlicesEqual checks whether two slices are element-wise equal. +func SlicesEqual[T comparable](a, b []T) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if a[i] != b[i] { + return false + } + } + return true +} + +// MapSlice returns a new slice by mapping given function over the input slice. +func MapSlice[T, V any](in []T, mapf func(T) V) []V { + ret := make([]V, len(in)) + for idx := range in { + ret[idx] = mapf(in[idx]) + } + return ret +} diff --git a/libpf/hash/hash.go b/libpf/hash/hash.go new file mode 100644 index 00000000..96ba121a --- /dev/null +++ b/libpf/hash/hash.go @@ -0,0 +1,31 @@ +/* + * 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 hash provides the same hash primitives as used by the eBPF. +// This file should be kept in sync with the eBPF tracemgmt.h. +package hash + +// Uint32 computes a hash of a 32-bit uint using the finalizer function for Murmur. +// 32-bit via https://en.wikipedia.org/wiki/MurmurHash#Algorithm +func Uint32(x uint32) uint32 { + x ^= x >> 16 + x *= 0x85ebca6b + x ^= x >> 13 + x *= 0xc2b2ae35 + x ^= x >> 16 + return x +} + +// Uint64 computes a hash of a 64-bit uint using the finalizer function for Murmur3 +// Via https://lemire.me/blog/2018/08/15/fast-strongly-universal-64-bit-hashing-everywhere/ +func Uint64(x uint64) uint64 { + x ^= x >> 33 + x *= 0xff51afd7ed558ccd + x ^= x >> 33 + x *= 0xc4ceb9fe1a85ec53 + x ^= x >> 33 + return x +} diff --git a/libpf/hash/hash_test.go b/libpf/hash/hash_test.go new file mode 100644 index 00000000..ceff9c4f --- /dev/null +++ b/libpf/hash/hash_test.go @@ -0,0 +1,59 @@ +/* + * 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 hash + +import ( + "math" + "testing" +) + +func TestUint64(t *testing.T) { + tests := map[string]struct { + input uint64 + expect uint64 + }{ + "0": {input: 0, expect: 0}, + "1": {input: 1, expect: 12994781566227106604}, + "uint16 max": {input: uint64(math.MaxUint16), expect: 6444452806975366496}, + "uint32 max": {input: uint64(math.MaxUint32), expect: 14731816277868330182}, + "uint64 max": {input: math.MaxUint64, expect: 7256831767414464289}, + } + + for name, testcase := range tests { + name := name + 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) + } + }) + } +} + +func TestUint32(t *testing.T) { + tests := map[string]struct { + input uint32 + expect uint32 + }{ + "0": {input: 0, expect: 0}, + "1": {input: 1, expect: 1364076727}, + "uint16 max": {input: uint32(math.MaxUint16), expect: 2721820263}, + "uint32 max": {input: math.MaxUint32, expect: 2180083513}, + } + + for name, testcase := range tests { + name := name + 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) + } + }) + } +} diff --git a/libpf/libpf.go b/libpf/libpf.go new file mode 100644 index 00000000..ffe36602 --- /dev/null +++ b/libpf/libpf.go @@ -0,0 +1,593 @@ +/* + * 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" + "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" +) + +// UnixTime32 is another type to represent seconds since epoch. +// In most cases 32bit time values are good enough until year 2106. +// Our time series database backend uses this type for TimeStamps as well, +// so there is no need to use a different type than uint32. +// Also, Go's semantics on map[time.Time] are particularly nasty footguns, +// and since the code is mostly dealing with UNIX timestamps, we may +// as well use uint32s instead. +// 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() +} + +// Compile-time interface checks +var _ json.Marshaler = (*UnixTime32)(nil) + +// NowAsUInt32 is a convenience function to avoid code repetition +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() +} + +// 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 + } + 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 +} + +// 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 + } + 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()) +} + +// Compile-time interface checks +var _ encoding.TextUnmarshaler = (*TraceHash)(nil) +var _ encoding.TextMarshaler = (*TraceHash)(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. +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 +} + +type FrameMetadata struct { + FileID FileID + AddressOrLine AddressOrLineno + LineNumber 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": +// +// The struct type with no fields is called the empty struct, written struct{}. It has size zero +// and carries no information but may be useful nonetheless. Some Go programmers +// use it instead of bool as the value type of a map that represents a set, to emphasize +// 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 new file mode 100644 index 00000000..5476ab0c --- /dev/null +++ b/libpf/libpf_test.go @@ -0,0 +1,306 @@ +/* + * 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" + + 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 + str string + }{ + { + ty: AbortFrame, + isErr: true, + interp: UnknownInterp, + str: "abort-marker", + }, + { + ty: PythonFrame, + isErr: false, + interp: Python, + str: "python", + }, + { + ty: NativeFrame.Error(), + isErr: true, + interp: Native, + str: "native-error", + }, + } + + for _, test := range tests { + assert.Equal(t, test.isErr, test.ty.IsError()) + assert.Equal(t, test.interp, test.ty.Interpreter()) + assert.Equal(t, test.str, test.ty.String()) + } +} diff --git a/libpf/lpm/lpm.go b/libpf/lpm/lpm.go new file mode 100644 index 00000000..41e29845 --- /dev/null +++ b/libpf/lpm/lpm.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. + */ + +// 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 new file mode 100644 index 00000000..a6c38ed4 --- /dev/null +++ b/libpf/lpm/lpm_test.go @@ -0,0 +1,84 @@ +//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/memorydebug/README.md b/libpf/memorydebug/README.md new file mode 100644 index 00000000..019cc3ae --- /dev/null +++ b/libpf/memorydebug/README.md @@ -0,0 +1,11 @@ +This package contains code to add memory profiling and the automated writing of heapdumps and heap +profiling samples to a given Go program, and uses build tags to only provide that functionality in +debug builds. + +The profiling agent uses this functionality at the moment. To enable it, do ```go build -tags debug``` +for the host agent. When running the agent with -v, it will log debug output showing memory +allocations, and also write memory profiles if 50 megabytes heap usage is exceeded to /tmp. It +will also write full heap dumps if heap usage exceeds 100 megabytes. + +You can inspect the heap profiles using "go tool pprof (filename)", and then typing "web" or "text" +to either inspect graphical output or a text-based heap profile. diff --git a/libpf/memorydebug/memorydebug_debug.go b/libpf/memorydebug/memorydebug_debug.go new file mode 100644 index 00000000..54d0ae14 --- /dev/null +++ b/libpf/memorydebug/memorydebug_debug.go @@ -0,0 +1,113 @@ +//go:build debug +// +build debug + +/* + * 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 memorydebug + +import ( + "bufio" + "bytes" + "fmt" + "net/http" + _ "net/http/pprof" + "os" + "runtime" + "runtime/debug" + "runtime/pprof" + "strconv" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +// If memory usage during a call to DebugLogMemoryUsage() exceeds the threshold in this +// variable, the code will write a full heapdump with timestamp to /tmp +var heapDumpLimit uint64 + +// If memory usage during a call to DebugLogMemoryUsage() exceeds the threshold in this +// variable, the code will write a memory profile to /tmp +var profileDumpLimit uint64 + +// Init sets up the limits for memory debugging: At what amount of heap usage should heap dumps +// or heap profiles be written. It also starts a web server on port 6060 so that live memory +// profiles can be pulled. +func Init(maxHeapBeforeDump, maxHeapBeforeProfile uint64) { + log.Debug("Initializing memory usag debugging.") + heapDumpLimit = maxHeapBeforeDump + profileDumpLimit = maxHeapBeforeProfile + // Start a local webserver to serve pprof profiles. + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() +} + +// readOwnRSS reads /proc/self/status to parse the usage of the resident set for the +// current process. We could parse /proc/self/statm instead (cheaper), but given that +// we won't do this often and only in debug, "expensive" parsing of /proc/self/status +// is fine. +func readOwnRSS() (rssAnon uint64, rssFile uint64, rssShmem uint64) { + contents, err := os.ReadFile("/proc/self/status") + if err != nil { + log.Fatalf("Reading our own RSS should never fail") + return 0, 0, 0 + } + scanner := bufio.NewScanner(bytes.NewReader(contents)) + for scanner.Scan() { + line := scanner.Text() + // Ignoring errors in the following lines is fine -- RssAnon and RssFile can never be + // zero for a live process, so problems are immediately evident. + if strings.HasPrefix(line, "RssAnon:") { + rssAnon, _ = strconv.ParseUint(strings.TrimSpace(line[10:len(line)-3]), 10, 64) + } else if strings.HasPrefix(line, "RssFile:") { + rssFile, _ = strconv.ParseUint(strings.TrimSpace(line[10:len(line)-3]), 10, 64) + } else if strings.HasPrefix(line, "RssShmem:") { + rssShmem, _ = strconv.ParseUint(strings.TrimSpace(line[10:len(line)-3]), 10, 64) + } + } + return rssAnon * 1024, rssFile * 1024, rssShmem * 1024 +} + +// DebugLogMemoryUsage is a no-op in release mode. In debug mode, it asks the runtime +// about actual memory use, logs information about the usage, and if the configured +// thresholds are exceeded dumps full memory logs to /tmp +func DebugLogMemoryUsage() { + // Read sizes of resident sets. + rssAnon, rssFile, rssShmem := readOwnRSS() + // Read the memory statistics from the Go runtime. + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + // Output the results. + log.Debugf("Alloc: %d Sys: %d Mallocs: %d Frees: %d HeapAlloc: %d HeapSys: %d RssAnon: %d RssFile: %d RssShmem: %d", + stats.Alloc, stats.Sys, stats.Mallocs, stats.Frees, stats.HeapAlloc, stats.HeapSys, rssAnon, + rssFile, rssShmem) + + // If the number of allocated bytes ever exceeds heapDumpLimit, make a heap dump. + if stats.Alloc > heapDumpLimit { + filename := fmt.Sprintf("/tmp/heap_dump_%d_%d", os.Getpid(), int32(time.Now().Unix())) + f, err := os.Create(filename) + defer f.Close() + if err != nil { + panic(err) + } + log.Debugf("Writing heap dump...") + debug.WriteHeapDump(f.Fd()) + } + if stats.Alloc > profileDumpLimit { + filename := fmt.Sprintf("/tmp/pprof_dump_%d_%d", os.Getpid(), int32(time.Now().Unix())) + f, err := os.Create(filename) + defer f.Close() + if err != nil { + panic(err) + } + log.Debugf("Writing heap profile...") + w := bufio.NewWriter(f) + pprof.Lookup("heap").WriteTo(w, 0) + w.Flush() + } +} diff --git a/libpf/memorydebug/memorydebug_release.go b/libpf/memorydebug/memorydebug_release.go new file mode 100644 index 00000000..c0fd2c33 --- /dev/null +++ b/libpf/memorydebug/memorydebug_release.go @@ -0,0 +1,16 @@ +//go:build !debug +// +build !debug + +/* + * 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 memorydebug + +func Init(int64, int64) { +} + +func DebugLogMemoryUsage() { +} diff --git a/libpf/memorydebug/memorydebug_test.go b/libpf/memorydebug/memorydebug_test.go new file mode 100644 index 00000000..e24d92e2 --- /dev/null +++ b/libpf/memorydebug/memorydebug_test.go @@ -0,0 +1,19 @@ +//go:build debug +// +build debug + +/* + * 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 memorydebug + +import "testing" + +func TestReadOwnRSS(t *testing.T) { + rssAnon, rssFile, rssShmem := readOwnRSS() + if rssAnon == 0 && rssFile == 0 && rssShmem == 0 { + t.Fatalf("At least one of the RSS values should be non-zero.") + } +} diff --git a/libpf/nativeunwind/elfunwindinfo/README.md b/libpf/nativeunwind/elfunwindinfo/README.md new file mode 100644 index 00000000..11b4b110 --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/README.md @@ -0,0 +1,50 @@ +# Extracting stack deltas from DWARF + +The code in this directory is responsible for extracting stack deltas from the +DWARF information in executables or their debug symbols. + +## What are stack deltas? + +In order to unwind native stack frames if the frame pointer has been omitted, it +is necessary to locate the "top of the function frame" when given a RIP. This is +usually at some offset from the current stack pointer RSP - the stack pointer can +vary throughout the function by pushing arguments to called functions etc. + +In short, for every RIP in a given binary, there is an associated value that can +provide the "top" of the function frame, e.g. the address where the return +address is stored, if added to RSP. We call this value 'stack delta'. + +## From where can we obtain stack deltas? + +The "safest" way to obtain them would be to perform a full disassembly and then +to track the stack pointer accordingly. This would deal with hand-written code +where the compiler cannot generate debug information etc. + +This is too time-consuming to develop at the moment. As a stopgap, it turns out +that modern ELF executables (e.g. those compiled in the last few years) have all +the necessary information to obtain the stack deltas in the `.eh_frame` section; +it is placed there to enable stack unwinding for C++ exceptions. This is very +useful for us. + +## How do we obtain stack deltas from the `.eh_frame` section? + +The section is in almost the same format as the `.debug_frame` section in the +debugging symbols. The DWARF format is optimized to both minimize the necessary +storage as well as supporting 20+ different CPU architectures; the solution in +DWARF is a fairly involved bytecode format and a small VM that allows the +calculation of the stack delta given an address. + +Fortunately the `.eh_frame` can contain only a fraction of DWARF commands, so +we implement that ourselves to parse efficiently the stack deltas, and other +needed information such RBP location in CFA to recover it. + +## Future work? + +The `.eh_frame` section is often buggy, not all compilers generate it, and there +are many other problems with it. The approach (disassemble & reconstruct) +discussed above was implemented by researchers; the following presentation gives +a good overview of the challenges and problems of working with DWARF (as well +as references to papers that validate unwind tables etc.) + +https://entropy2018.sciencesconf.org/data/nardelli.pdf + diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe.go b/libpf/nativeunwind/elfunwindinfo/elfehframe.go new file mode 100644 index 00000000..1e37339e --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/elfehframe.go @@ -0,0 +1,1268 @@ +/* + * 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 ( + "bytes" + "debug/elf" + "encoding/hex" + "errors" + "fmt" + "unsafe" + + lru "github.com/elastic/go-freelru" + 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" +) + +const ( + // Most files have single CIE, and all FDEs use that. But multiple CIEs are needed + // in some cases. E.g. glibc has 4 CIEs. 16 was chosen to be generous and make sure + // CIEs would not need to be reparsed. + cieCacheSize = 16 + + // Maximum bytes in .eh_frame (xul has about 16M) + maxBytesEHFrame = 64 * 1024 * 1024 +) + +// errUnexpectedType is used internally to detect inconsistent FDE/CIE types +var errUnexpectedType = errors.New("unexpected FDE/CIE type") + +// ehframeHooks interface provides hooks for filtering and debugging eh_frame parsing +type ehframeHooks interface { + // fdeHook is called for each FDE. Returns false if the FDE should be filtered out. + fdeHook(cie *cieInfo, fde *fdeInfo) bool + // deltaHook is called for each stack delta found + deltaHook(ip uintptr, regs *vmRegs, delta sdtypes.StackDelta) +} + +// uleb128 is the data type for unsigned little endian base-128 encoded number +type uleb128 uint64 + +// sleb128 is the data type for signed little endian base-128 encoded number +type sleb128 int64 + +// DWARF Call Frame Instructions +// http://dwarfstd.org/doc/DWARF5.pdf §6.4.2 +// https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/dwarfext.html +type cfaOpcode uint8 + +const ( + cfaNop cfaOpcode = 0x00 + cfaSetLoc cfaOpcode = 0x01 + cfaAdvanceLoc1 cfaOpcode = 0x02 + cfaAdvanceLoc2 cfaOpcode = 0x03 + cfaAdvanceLoc4 cfaOpcode = 0x04 + cfaOffsetExtended cfaOpcode = 0x05 + cfaRestoreExtended cfaOpcode = 0x06 + cfaUndefined cfaOpcode = 0x07 + cfaSameValue cfaOpcode = 0x08 + cfaRegister cfaOpcode = 0x09 + cfaRememberState cfaOpcode = 0x0a + cfaRestoreState cfaOpcode = 0x0b + cfaDefCfa cfaOpcode = 0x0c + cfaDefCfaRegister cfaOpcode = 0x0d + cfaDefCfaOffset cfaOpcode = 0x0e + cfaDefCfaExpression cfaOpcode = 0x0f + cfaExpression cfaOpcode = 0x10 + cfaOffsetExtendedSf cfaOpcode = 0x11 + cfaDefCfaSf cfaOpcode = 0x12 + cfaDefCfaOffsetSf cfaOpcode = 0x13 + cfaValOffset cfaOpcode = 0x14 + cfaValOffsetSf cfaOpcode = 0x15 + cfaValExpression cfaOpcode = 0x16 + cfaGNUWindowSave cfaOpcode = 0x2d + cfaGNUArgsSize cfaOpcode = 0x2e + cfaGNUNegOffsetExtended cfaOpcode = 0x2f + cfaAdvanceLoc cfaOpcode = 0x40 + cfaOffset cfaOpcode = 0x80 + cfaRestore cfaOpcode = 0xc0 + cfaHighOpcodeMask cfaOpcode = 0xc0 + cfaHighOpcodeValueMask cfaOpcode = 0x3f +) + +// DWARF Expression Opcodes +// http://dwarfstd.org/doc/DWARF5.pdf §2.5, §7.7.1 +// The subset needed for normal .eh_frame handling +type expressionOpcode uint8 + +//nolint:deadcode,varcheck +const ( + opDeref expressionOpcode = 0x06 + opConstU expressionOpcode = 0x10 + opConstS expressionOpcode = 0x11 + opRot expressionOpcode = 0x17 + opAnd expressionOpcode = 0x1a + opMul expressionOpcode = 0x1e + opPlus expressionOpcode = 0x22 + opPlusUConst expressionOpcode = 0x23 + opShl expressionOpcode = 0x24 + opGE expressionOpcode = 0x2a + opNE expressionOpcode = 0x2e + opLit0 expressionOpcode = 0x30 + opBReg0 expressionOpcode = 0x70 +) + +type dwarfExpression struct { + opcode expressionOpcode + operand1 uleb128 + operand2 uleb128 +} + +// DWARF Exception Header Encoding +// https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/dwarfext.html +type encoding uint8 + +//nolint:deadcode,varcheck +const ( + encFormatNative encoding = 0x00 + encFormatLeb128 encoding = 0x01 + encFormatData2 encoding = 0x02 + encFormatData4 encoding = 0x03 + encFormatData8 encoding = 0x04 + encFormatMask encoding = 0x07 + encSignedMask encoding = 0x08 + encAdjustAbs encoding = 0x00 + encAdjustPcRel encoding = 0x10 + encAdjustTextRel encoding = 0x20 + encAdjustDataRel encoding = 0x30 + encAdjustFuncRel encoding = 0x40 + encAdjustAligned encoding = 0x50 + encAdjustMask encoding = 0x70 + encIndirect encoding = 0x80 + encOmit encoding = 0xff +) + +// Exception Frame Header (.eh_frame_hdr section) +// https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html +type ehFrameHdr struct { + version uint8 + ehFramePtrEnc encoding + fdeCountEnc encoding + tableEnc encoding + // Continued with the following: + // ehFramePtr ptr{ehFramePtrEnc} + // fdeCount ptr{ehFramePtrEnc} + // searchTable [fdeCount]struct { + // startIp ptr{tableEnc} + // fdeAddr ptr{tableEnc} + // } +} + +// reader provides read access to the Exception Frame section and the virtual address base. +type reader struct { + debugFrame bool + + data []byte + pos uintptr + end uintptr + vaddr uintptr +} + +// hasData checks if the reader is still in valid state +func (r *reader) hasData() bool { + return r.pos < r.end +} + +// isValid checks if the reader is still in valid state +func (r *reader) isValid() bool { + return r.pos <= r.end +} + +// get gets a pointer for n bytes of data, and advances the current position +func (r *reader) get(n uintptr) unsafe.Pointer { + pos := r.pos + r.pos += n + if !r.isValid() { + // Return valid pointer to zero data of up to 8-bytes so the + // following accessors can dereference the return value. + // If an overread happened, it is detected at the end of parsing + // an block with isValid() call to see if r.pos is still within + // correct bounds. + v := uint64(0) + return unsafe.Pointer(&v) + } + return unsafe.Pointer(&r.data[pos]) +} + +// u8 reads one unsigned byte +func (r *reader) u8() uint8 { + return *(*uint8)(r.get(1)) +} + +// u16 reads one unsigned 16-bit word +func (r *reader) u16() uint16 { + return *(*uint16)(r.get(2)) +} + +// u32 reads one unsigned 32-bit word +func (r *reader) u32() uint32 { + return *(*uint32)(r.get(4)) +} + +// u64 reads one unsigned 64-bit word +func (r *reader) u64() uint64 { + return *(*uint64)(r.get(8)) +} + +// uleb reads one unsigned little endian base-128 encoded value +func (r *reader) uleb() uleb128 { + b := uint8(0x80) + val := uleb128(0) + for shift := 0; b&0x80 != 0; shift += 7 { + b = r.u8() + val |= uleb128(b&0x7f) << shift + } + return val +} + +// sleb reads one signed little endian base-128 encoded value +func (r *reader) sleb() sleb128 { + b := uint8(0x80) + val := sleb128(0) + shift := 0 + for ; b&0x80 != 0; shift += 7 { + b = r.u8() + val |= sleb128(b&0x7f) << shift + } + if b&0x40 != 0 { + // Sign extend + val |= sleb128(-1) << shift + } + return val +} + +// str reads one zero-terminated string value +func (r *reader) str() []byte { + cur := r.pos + end := r.pos + for r.data[end] != 0 { + end++ + } + r.pos = end + 1 + return r.data[cur:end] +} + +// bytes reads one n-length byte array value +func (r *reader) bytes(num uintptr) []byte { + cur := r.pos + end := r.pos + num + r.pos = end + if !r.isValid() { + return nil + } + return r.data[cur:end] +} + +// expression reads one DWARF expression, and normalizes it in the sense that +// opcodes are returned in indexable slice and each opcode with operand is +// adjusted to it's basic value with operand separated. The concept is to allow +// pattern matching expression with opcodes sequences. +func (r *reader) expression() ([]dwarfExpression, error) { + blen := uintptr(r.uleb()) + data := r.bytes(blen) + if data == nil { + return nil, fmt.Errorf("expression data missing") + } + ed := reader{ + data: data, + end: blen, + } + expr := make([]dwarfExpression, 0, 8) + for ed.hasData() { + op := expressionOpcode(ed.u8()) + switch { + case op >= opLit0 && op <= opLit0+31: + expr = append(expr, dwarfExpression{ + opcode: opLit0, + operand1: uleb128(op - opLit0), + }) + case op >= opBReg0 && op <= opBReg0+31: + expr = append(expr, dwarfExpression{ + opcode: opBReg0, + operand1: uleb128(op - opBReg0), + operand2: uleb128(ed.sleb()), + }) + case op == opConstU, op == opPlusUConst: + expr = append(expr, dwarfExpression{ + opcode: op, + operand1: ed.uleb(), + }) + case op == opConstS: + expr = append(expr, dwarfExpression{ + opcode: op, + operand1: uleb128(ed.sleb()), + }) + case op == opDeref, op >= opRot && op <= opNE: + expr = append(expr, dwarfExpression{opcode: op}) + default: + return nil, fmt.Errorf("unsupported expression: %s", + hex.EncodeToString(data)) + } + } + return expr, nil +} + +// ptr reads one pointer value encoded with enc encoding +func (r *reader) ptr(enc encoding) (uintptr, error) { + if enc == encOmit { + return 0, nil + } + pos := r.pos + var val uintptr + switch enc & (encFormatMask | encSignedMask) { + case encFormatData2: + val = uintptr(r.u16()) + case encFormatData4: + val = uintptr(r.u32()) + case encFormatData8, encFormatNative, encFormatData8 | encSignedMask: + val = uintptr(r.u64()) + case encFormatData2 | encSignedMask: + val = uintptr(int64(*(*int16)(r.get(2)))) + case encFormatData4 | encSignedMask: + val = uintptr(int64(*(*int32)(r.get(4)))) + default: + return 0, fmt.Errorf("unsupported format encoding %#02x", enc) + } + + switch enc & encAdjustMask { + case encAdjustAbs: + case encAdjustPcRel: + val += pos + r.vaddr + case encAdjustDataRel: + val += r.vaddr + default: + return 0, fmt.Errorf("unsupported adjust encoding %#02x", enc) + } + + if enc&encIndirect != 0 { + return 0, fmt.Errorf("unsupported indirect encoding %#02x", enc) + } + + return val, nil +} + +// cieInfo describes the contents of one Common Information Entry (CIE) +type cieInfo struct { + dataAlign sleb128 + codeAlign uleb128 + regRA uleb128 + enc encoding + ldsaEnc encoding + hasAugmentation bool + isSignalHandler bool + + // initialState is the virtual machine state after running CIE opcodes + initialState vmRegs +} + +// fdeInfo contains one Frame Description Entry (FDE) +type fdeInfo struct { + len uint64 + ciePos uint64 + ipLen uintptr + ipStart uintptr + sorted bool +} + +const ( + // extensions values used internally + regUndefined uleb128 = 128 + regCFA uleb128 = 129 + regCFAVal uleb128 = 130 + regSame uleb128 = 131 + regExprPLT uleb128 = 256 + regExprRegDeref uleb128 = 257 + regExprRegRegDeref uleb128 = 258 + regExprReg uleb128 = 259 +) + +// sigretCodeMap contains the per-machine trampoline to call rt_sigreturn syscall. +// This is needed to detect signal trampoline functions as the .eh_frame often +// does not contain the proper unwind info due to various reasons. +// nolint:lll +var sigretCodeMap = map[elf.Machine][]byte{ + elf.EM_AARCH64: { + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/kernel/vdso/sigreturn.S?h=v6.4#n71 + // https://git.musl-libc.org/cgit/musl/tree/src/signal/aarch64/restore.s?h=v1.2.4#n9 + // movz x8, #0x8b + 0x68, 0x11, 0x80, 0xd2, + // svc #0x0 + 0x01, 0x00, 0x00, 0xd4, + }, + elf.EM_X86_64: { + // https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/x86_64/libc_sigaction.c;h=afdce87381228f0cf32fa9fa6c8c4efa5179065c;hb=a704fd9a133bfb10510e18702f48a6a9c88dbbd5#l80 + // https://git.musl-libc.org/cgit/musl/tree/src/signal/x86_64/restore.s?h=v1.2.4#n6 + // mov $0xf,%rax + 0x48, 0xc7, 0xc0, 0x0f, 0x00, 0x00, 0x00, + // syscall + 0x0f, 0x05, + }, +} + +// vmReg describes the register unwinding state in dwarf virtual machine +type vmReg struct { + arch elf.Machine + // reg is the register or extension base to use + reg uleb128 + // off is the offset to add to the base + off sleb128 +} + +// makeOff encodes four 16-bit integers into vmReg.off field to be used as expression parameters +func makeOff(a, b, c, d int16) sleb128 { + return sleb128((uleb128(uint16(a)) << 48) + (uleb128(uint16(b)) << 32) + + (uleb128(uint16(c)) << 16) + uleb128(uint16(d))) +} + +// splitOff undoes makeOff and splits the vmReg.off to 16-bit integers +func splitOff(off sleb128) (a, b, c, d int16) { + return int16(off >> 48), int16(off >> 32), int16(off >> 16), int16(off) +} + +// getCFARegName converts internally used register descriptions into a string +func getCFARegName(reg uleb128) string { + switch reg { + case regCFA: + return "c" + case regCFAVal: + return "&c" + case regUndefined: + return "u" + case regSame: + return "s" + default: + return fmt.Sprintf("r%d", reg) + } +} + +// getRegName converts register index to a string describing the register +func getRegName(arch elf.Machine, reg uleb128) string { + switch { + case reg >= regUndefined: + return getCFARegName(reg) + case arch == elf.EM_AARCH64: + return getRegNameARM(reg) + case arch == elf.EM_X86_64: + return getRegNameX86(reg) + default: + log.Errorf("Unexpected register index value: %d", reg) + return fmt.Sprintf("unk%d", reg) + } +} + +// String will format the unwinding rule for 'reg' as a string +func (reg *vmReg) String() string { + if reg.reg < regExprPLT { + name := getRegName(reg.arch, reg.reg) + if reg.off == 0 { + return name + } + return fmt.Sprintf("%s%+d", name, reg.off) + } + switch reg.reg { + case regExprPLT: + return "plt" + case regExprReg: + a, _, b, _ := splitOff(reg.off) + return fmt.Sprintf("%s%+d", getRegName(reg.arch, uleb128(a)), b) + case regExprRegDeref: + a, _, b, c := splitOff(reg.off) + return fmt.Sprintf("*(%s%+d)%+d", + getRegName(reg.arch, uleb128(a)), b, c) + case regExprRegRegDeref: + a, b, c, d := splitOff(reg.off) + return fmt.Sprintf("*(%s+8*%s+%d)%+d", + getRegName(reg.arch, uleb128(a)), getRegName(reg.arch, uleb128(b)), c, d) + default: + return "?" + } +} + +// expression recognizes the argument expression and sets the vmReg value to it +func (reg *vmReg) expression(expr []dwarfExpression) error { + reg.reg = regUndefined + reg.off = 0 + + // Support is included for few selected expression + switch { + case matchExpression(expr, []expressionOpcode{opBReg0, opBReg0, opLit0, opAnd, + opLit0, opGE, opLit0, opShl, opPlus}): + // Assume this sequence is the PLT expression generated by GCC, + // regardless of the operand values + reg.reg = regExprPLT + case matchExpression(expr, []expressionOpcode{opBReg0}): + // Register dereference expression (seen for registers in SSE vectorized code) + reg.reg = regExprReg + reg.off = makeOff(int16(expr[0].operand1), 0, int16(expr[0].operand2), 0) + case matchExpression(expr, []expressionOpcode{opBReg0, opDeref}): + // Register dereference expression (seen for CFA in SSE vectorized code) + reg.reg = regExprRegDeref + reg.off = makeOff(int16(expr[0].operand1), 0, int16(expr[0].operand2), 0) + case matchExpression(expr, []expressionOpcode{opBReg0, opDeref, opPlusUConst}): + // Register dereference expression (seen in openssl libcrypto) + reg.reg = regExprRegDeref + reg.off = makeOff(int16(expr[0].operand1), 0, int16(expr[0].operand2), + int16(expr[2].operand1)) + case matchExpression(expr, []expressionOpcode{opBReg0, opBReg0, opLit0, opMul, + opPlus, opDeref, opPlusUConst}) && + expr[1].operand2 == 0 && expr[2].operand1 == 8: + // Register + register dereference expression (seen in openssl libcrypto) + reg.reg = regExprRegRegDeref + reg.off = makeOff( + int16(expr[0].operand1), int16(expr[1].operand1), + int16(expr[0].operand2), int16(expr[6].operand1)) + default: + return fmt.Errorf("DWARF expression unmatched: %x", expr) + } + return nil +} + +// vmRegs contains the dwarf virtual machine registers we track +type vmRegs struct { + arch elf.Machine + cfa vmReg + // generic (platform independent) DWARF registers for frame pointer + // and return address access + fp, ra vmReg +} + +// reg returns the address to vmReg description of the given numeric register +func (regs *vmRegs) reg(ndx uleb128) *vmReg { + switch regs.arch { + case elf.EM_AARCH64: + return regs.regARM(ndx) + case elf.EM_X86_64: + return regs.regX86(ndx) + default: + return nil + } +} + +// state is the virtual machine state which can execute exception handler opcodes +type state struct { + // cie is the CIE being currently processed + cie *cieInfo + // loc is the current location (RIP) + loc uintptr + // cur is the current state of the virtual machine + cur vmRegs + // stash is the implicit stack of register states for remember/restore opcodes + stack [2]vmRegs + // stackNdx is the current stack nesting level for remember/restore opcodes + stackNdx int +} + +// advance increments current virtual address by given delta and code alignment +func (st *state) advance(delta int) { + st.loc += uintptr(delta * int(st.cie.codeAlign)) +} + +// rule assign an unwinding rule for given register 'reg' +func (st *state) rule(reg, baseReg uleb128, off sleb128) { + r := st.cur.reg(reg) + if r != nil { + r.reg = baseReg + r.off = off * st.cie.dataAlign + } +} + +// restore assigns given numeric register it's original value after CIE opcodes +func (st *state) restore(reg uleb128) { + if to := st.cur.reg(reg); to != nil { + *to = *st.cie.initialState.reg(reg) + } +} + +// matchExpression compares if the opcodes of expr match the template given +func matchExpression(expr []dwarfExpression, template []expressionOpcode) bool { + if len(expr) != len(template) { + return false + } + for i := 0; i < len(expr); i++ { + if expr[i].opcode != template[i] { + return false + } + } + return true +} + +// step executes the EH virtual opcodes until a new virtual address is encountered +// or end of opcodes is reached. +func (st *state) step(r *reader) error { + var err error + + for r.hasData() { + opcode := cfaOpcode(r.u8()) + operand := uint8(0) + + // If the high opcode bits are set, the upper bits are opcode + // and the lower bits is operand. + if opcode&cfaHighOpcodeMask != 0 { + operand = uint8(opcode & cfaHighOpcodeValueMask) + opcode &= cfaHighOpcodeMask + } + + // Handle the opcode + switch opcode { + case cfaNop: + // Nothing to do! + case cfaSetLoc: + st.loc, err = r.ptr(st.cie.enc) + return err + case cfaAdvanceLoc1: + st.advance(int(r.u8())) + return nil + case cfaAdvanceLoc2: + st.advance(int(r.u16())) + return nil + case cfaAdvanceLoc4: + st.advance(int(r.u32())) + return nil + case cfaOffsetExtended: + st.rule(r.uleb(), regCFA, sleb128(r.uleb())) + case cfaRestoreExtended: + st.restore(r.uleb()) + case cfaUndefined: + st.rule(r.uleb(), regUndefined, 0) + case cfaSameValue: + st.rule(r.uleb(), regSame, 0) + case cfaRegister: + st.rule(r.uleb(), r.uleb(), 0) + case cfaRememberState: + if st.stackNdx >= len(st.stack) { + return fmt.Errorf("dwarf stack overflow at %x", + st.loc) + } + st.stack[st.stackNdx] = st.cur + st.stackNdx++ + case cfaRestoreState: + if st.stackNdx == 0 { + return fmt.Errorf("dwarf stack underflow at %x", + st.loc) + } + st.stackNdx-- + st.cur = st.stack[st.stackNdx] + case cfaDefCfa: + st.cur.cfa.reg = r.uleb() + st.cur.cfa.off = sleb128(r.uleb()) + case cfaDefCfaRegister: + st.cur.cfa.reg = r.uleb() + case cfaDefCfaOffset: + st.cur.cfa.off = sleb128(r.uleb()) + case cfaDefCfaExpression: + expr, err := r.expression() + if err == nil { + err = st.cur.cfa.expression(expr) + } + if err != nil { + log.Debugf("DWARF expression error (CFA): %v", err) + } + case cfaExpression: + reg := r.uleb() + expr, err := r.expression() + if r := st.cur.reg(reg); err == nil && r != nil { + err = r.expression(expr) + if err != nil && reg == x86RegRBP { + log.Debugf("DWARF expression error (RBP): %v", err) + } + } + case cfaOffsetExtendedSf: + st.rule(r.uleb(), regCFA, r.sleb()) + case cfaDefCfaSf: + st.cur.cfa.reg = r.uleb() + st.cur.cfa.off = r.sleb() * st.cie.dataAlign + case cfaDefCfaOffsetSf: + st.cur.cfa.off = r.sleb() * st.cie.dataAlign + case cfaValOffset: + st.rule(r.uleb(), regCFAVal, sleb128(r.uleb())) + case cfaValOffsetSf: + st.rule(r.uleb(), regCFAVal, r.sleb()) + case cfaValExpression: + // Not really supported, just mark the register undefined + st.rule(r.uleb(), regUndefined, 0) + r.pos += uintptr(r.uleb()) + case cfaGNUWindowSave: + // No handling needed + case cfaGNUArgsSize: + // TODO: support this. It means there's callee removed + // arguments in the stack. Fortunately, it seems that + // RBP is often used as CFA base in these case, so this + // likely is does not need further support. + // At least glibc built libstdc++.so.6.0.25 had these. + r.uleb() + case cfaGNUNegOffsetExtended: + st.rule(r.uleb(), regCFA, -r.sleb()) + case cfaAdvanceLoc: + st.advance(int(operand)) + return nil + case cfaOffset: + st.rule(uleb128(operand), regCFA, sleb128(r.uleb())) + case cfaRestore: + st.restore(uleb128(operand)) + default: + return fmt.Errorf("DWARF opcode %#02x not implemented", + opcode) + } + } + return nil +} + +// parseHDR parses the common part of CIE and FDE blocks +// 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) parseHDR(expectCIE bool) (hlen, ciePos uint64, err error) { + var idPos, cieMarker uint64 + pos := r.pos + hlen = uint64(r.u32()) + if hlen < 0xfffffff0 { + // Normal 32-bit dwarf + hlen += 4 + idPos = uint64(r.pos) + ciePos = uint64(r.u32()) + cieMarker = 0xffffffff + } else if hlen == 0xffffffff { + // 64-bit dwarf + hlen = r.u64() + hlen += 4 + 8 + idPos = uint64(r.pos) + ciePos = r.u64() + cieMarker = 0xffffffffffffffff + } else { + return 0, 0, fmt.Errorf("unsupported initial length %#x", hlen) + } + r.end = pos + uintptr(hlen) + if r.end > uintptr(len(r.data)) { + return 0, 0, fmt.Errorf("CIE/FDE extends beyond end at %#x", r.pos) + } + if !r.debugFrame { + // In .eh_frame's the CIE marker pointer value is zero + cieMarker = 0 + } + isCIE := ciePos == cieMarker + if isCIE != expectCIE { + return hlen, 0, fmt.Errorf("CIE/FDE %#x: %w", ciePos, errUnexpectedType) + } + if !isCIE { + if !r.debugFrame { + // In .eh_frame, the FDE pointer is relative to its header position, + // not to the start of section. + ciePos = idPos - ciePos + } + if ciePos >= uint64(len(r.data)) { + return 0, 0, fmt.Errorf("FDE starts beyond end at %#x", ciePos) + } + } + return hlen, ciePos, nil +} + +// parseCIE reads and processes one Common Information Entry +// 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) parseCIE(cie *cieInfo) error { + _, _, err := r.parseHDR(true) + if err != nil { + return err + } + + ver := r.u8() + if ver != 1 && ver != 3 && ver != 4 { + return fmt.Errorf("CIE version %d not supported", ver) + } + + *cie = cieInfo{ + enc: encFormatNative | encAdjustAbs, + ldsaEnc: encFormatNative | encAdjustAbs, + } + + augmentation := r.str() + + if ver == 4 { + // CIE version 4 adds two new fields we don't make use of yet. But we need to + // read them so the rest of the data is aligned correctly. + + // Skip the address_size field + r.u8() + // Skip the segment_selector_size field + r.u8() + } + + cie.codeAlign = r.uleb() + cie.dataAlign = r.sleb() + if ver == 1 { + cie.regRA = uleb128(r.u8()) + } else { + cie.regRA = r.uleb() + } + + // A zero length string indicates that no augmentation data is present. + if len(augmentation) > 0 { + // Parse rest of CIE header based on augmentation string + if augmentation[0] != 'z' { + return fmt.Errorf("too old augmentation string '%s'", augmentation) + } + r.uleb() + cie.hasAugmentation = true + + for _, ch := range string(augmentation[1:]) { + switch ch { + case 'L': + cie.ldsaEnc = encoding(r.u8()) + case 'R': + cie.enc = encoding(r.u8()) + case 'P': + // remove the indirect as it's not supported, but we + // don't use the result here anyway + enc := encoding(r.u8()) &^ encIndirect + if _, err = r.ptr(enc); err != nil { + return err + } + case 'S': + cie.isSignalHandler = true + default: + return fmt.Errorf("unsupported augmentation string '%s'", + augmentation) + } + } + } + + if !r.isValid() { + return fmt.Errorf("CIE not valid after header") + } + return err +} + +// getUnwindInfo generates the needed unwind information from the register set +func (regs *vmRegs) getUnwindInfo() sdtypes.UnwindInfo { + switch regs.arch { + case elf.EM_AARCH64: + return regs.getUnwindInfoARM() + case elf.EM_X86_64: + return regs.getUnwindInfoX86() + default: + panic(fmt.Sprintf("architecture %d is not supported", regs.arch)) + } +} + +// newVMRegs initializes vmRegs structure for given architecture +func newVMRegs(arch elf.Machine) vmRegs { + switch arch { + case elf.EM_AARCH64: + return newVMRegsARM() + case elf.EM_X86_64: + return newVMRegsX86() + default: + panic(fmt.Sprintf("architecture %d is not supported", arch)) + } +} + +// isSignalTrampoline matches a given FDE against well known signal return handler +// code sequence. +func isSignalTrampoline(efCode *pfelf.File, fde *fdeInfo) bool { + sigretCode, ok := sigretCodeMap[efCode.Machine] + if !ok { + return false + } + if fde.ipLen != uintptr(len(sigretCode)) { + return false + } + fdeCode := make([]byte, len(sigretCode)) + if _, err := efCode.ReadVirtualMemory(fdeCode, int64(fde.ipStart)); err != nil { + return false + } + return bytes.Equal(fdeCode, sigretCode) +} + +// parseFDE reads and processes one Frame Description Entry and returns the size of +// the CIE/FDE entry, and amends the intervals to deltas table. +// 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) { + // Parse FDE header + fdeID := r.pos + fde := fdeInfo{sorted: sorted} + fde.len, fde.ciePos, err = r.parseHDR(false) + if err != nil { + // parseHDR returns unconditionally the CIE/FDE entry length. + // Also return the size here. This is to allow walkFDEs to use + // this function and skip CIEs. + return uintptr(fde.len), err + } + + // Calculate CIE location, and get and cache the CIE data + cie, ok := cieCache.Get(fde.ciePos) + if !ok { + cr := *r + cr.pos = uintptr(fde.ciePos) + + cie = &cieInfo{} + if err = cr.parseCIE(cie); err != nil { + return 0, fmt.Errorf("CIE %#x failed: %v", fde.ciePos, err) + } + + // initialize vmRegs from initialState - these can be used by restore + // opcode during initial CIE run + cie.initialState = newVMRegs(ef.Machine) + + // Run CIE initial opcodes + st := state{ + cie: cie, + cur: newVMRegs(ef.Machine), + } + if err = st.step(&cr); err != nil { + return 0, err + } + if !cr.isValid() { + return 0, fmt.Errorf("CIE %x parsing failed", fde.ciePos) + } + cie.initialState = st.cur + cieCache.Add(fde.ciePos, cie) + } + + // Parse rest of FDE structure (CIE dependent part) + st := state{cie: cie, cur: cie.initialState} + fde.ipStart, err = r.ptr(st.cie.enc) + if err != nil { + return 0, err + } + if ipStart != 0 && fde.ipStart != ipStart { + return 0, fmt.Errorf( + "FDE ipStart (%x) not matching search table FDE ipStart (%x)", + fde.ipStart, ipStart) + } + if st.cie.enc&encIndirect != 0 { + fde.ipLen, err = r.ptr(st.cie.enc) + } else { + fde.ipLen, err = r.ptr(st.cie.enc & (encFormatMask | encSignedMask)) + } + if err != nil { + return 0, err + } + + if st.cie.hasAugmentation { + r.pos += uintptr(r.uleb()) + } + if !r.isValid() { + return 0, fmt.Errorf("FDE %x not valid after header", fdeID) + } + + // Process the FDE opcodes + if hooks != nil && !hooks.fdeHook(st.cie, &fde) { + return uintptr(fde.len), nil + } + st.loc = fde.ipStart + + if st.cie.isSignalHandler || isSignalTrampoline(efCode, &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) + } else { + hint := sdtypes.UnwindHintKeep + for r.hasData() { + ip := st.loc + if err := st.step(r); err != nil { + return 0, err + } + delta := sdtypes.StackDelta{ + Address: uint64(ip), + Hints: hint, + Info: st.cur.getUnwindInfo(), + } + if hooks != nil { + hooks.deltaHook(ip, &st.cur, delta) + } + deltas.AddEx(delta, sorted) + hint = sdtypes.UnwindHintNone + } + + delta := sdtypes.StackDelta{ + Address: uint64(st.loc), + Hints: hint, + Info: st.cur.getUnwindInfo(), + } + deltas.AddEx(delta, sorted) + + if !r.isValid() { + return 0, fmt.Errorf("FDE %x parsing failed", fdeID) + } + } + + info := sdtypes.UnwindInfoInvalid + if ef.Entry == uint64(fde.ipStart+fde.ipLen) { + info = sdtypes.UnwindInfoStop + } + + // Add end-of-function stop delta. This might later get removed if there is + // another function starting on this address. + deltas.AddEx(sdtypes.StackDelta{ + Address: uint64(fde.ipStart + fde.ipLen), + Hints: sdtypes.UnwindHintGap, + Info: info, + }, sorted) + + return uintptr(fde.len), nil +} + +// elfRegion is a reference to a region within an ELF file. Such a region reference can be +// constructed from either an ELF section or an ELF program header. +type elfRegion struct { + data []byte + vaddr uintptr +} + +// reader creates a `reader` for this ELF region. +func (ref *elfRegion) reader(pos uintptr, debugFrame bool) reader { + return reader{ + debugFrame: debugFrame, + data: ref.data, + pos: pos, + end: uintptr(len(ref.data)), + vaddr: ref.vaddr, + } +} + +// elfRegionFromSection checks whether a given ELF section looks valid and has data, then +// creating a elfRegion for it. Otherwise, returns `nil`. +func elfRegionFromSection(sec *pfelf.Section) *elfRegion { + if sec == nil || sec.Type == elf.SHT_NOBITS { + return nil + } + + data, err := sec.Data(maxBytesEHFrame) + if err != nil { + return nil + } + + return &elfRegion{ + data: data, + vaddr: uintptr(sec.Addr), + } +} + +// validateEhFrameHdr checks whether the given `.eh_frame_hdr` section is in a format that we +// support for parsing, returning a pointer to the header struct. Otherwise, returns `nil`. +func validateEhFrameHdr(ehFrameHdrSec *elfRegion) *ehFrameHdr { + if ehFrameHdrSec == nil { + return nil + } + + if uintptr(len(ehFrameHdrSec.data)) < unsafe.Sizeof(ehFrameHdr{}) { + return nil + } + + // If the header version is not what we expect, we can't reasonably expect to be able to + // parse the eh_frame section. + h := (*ehFrameHdr)(unsafe.Pointer(&ehFrameHdrSec.data[0])) + if h.version != 1 { + return nil + } + + // If the binary search table is in an unsupported format or omitted, we just ignore it + // and go with the same approach as if the header wasn't present at all. + if h.tableEnc != encAdjustDataRel+encSignedMask+encFormatData4 { + return nil + } + + return h +} + +// findEhSections attempts multiple different methods of locating the .eh_frame_hdr and .eh_frame +// ELF sections. +func findEhSections(ef *pfelf.File) ( + ehFrameHdrSec *elfRegion, ehFrameSec *elfRegion, err error) { + ehFrameHdrSize := unsafe.Sizeof(ehFrameHdr{}) + + // Attempt to find .eh_frame{,_hdr} via their section header. This should work for the majority + // of well-behaved ELF binaries. + ehFrameSec = elfRegionFromSection(ef.Section(".eh_frame")) + ehFrameHdrSec = elfRegionFromSection(ef.Section(".eh_frame_hdr")) + + // Validate whether we can use the eh_frame_hdr section. + if hdr := validateEhFrameHdr(ehFrameHdrSec); hdr == nil { + ehFrameHdrSec = nil + } + + // If we at least have the eh_frame section now, we can early-exit. The code below is a bit + // more wobbly, so it's better to proceed without the header than to risk having to go with + // the fallback. + if ehFrameSec != nil { + return ehFrameHdrSec, ehFrameSec, nil + } + + // Attempt to locate the eh_frame section via the program headers. This is here to support + // coredump binaries and other ELF files that have the section headers stripped. + prog, err := ef.EHFrame() + if err != nil { + log.Debugf("No PT_GNU_EH_FRAME dynamic tag: %v", err) + return nil, nil, nil + } + + data, err := prog.Data(maxBytesEHFrame) + if err != nil { + return nil, nil, err + } + + if uintptr(len(data)) < ehFrameHdrSize { + return nil, nil, fmt.Errorf( + "located ELF region is too small to be a valid eh_frame header (%d bytes)", len(data)) + } + + ehFrameHdrSec = &elfRegion{ + data: data, + vaddr: uintptr(prog.Vaddr), + } + + // Validate .eh_frame_hdr section. + if hdr := validateEhFrameHdr(ehFrameHdrSec); hdr == nil { + // There is no DWARF tag for the eh_frame section, just for the header. If the header + // 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") + } + + ehFrameSec = &elfRegion{ + data: data[ehFrameHdrSize:], + vaddr: ehFrameHdrSec.vaddr + ehFrameHdrSize, + } + + // Some binaries only have the header, but no actual eh_frame section. This is, for example, + // 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 ehFrameHdrSec, ehFrameSec, nil +} + +// 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 { + h := (*ehFrameHdr)(unsafe.Pointer(&ehFrameHdrSec.data[0])) + + // Skip header, which is immediately followed by the binary search table. The header was + // already previously validated in `validateEhFrameHdr`. + r := ehFrameHdrSec.reader(unsafe.Sizeof(*h), false) + + if _, err := r.ptr(h.ehFramePtrEnc); err != nil { + return err + } + fdeCount, err := r.ptr(h.fdeCountEnc) + if err != nil { + return err + } + + cieCache, err := lru.New[uint64, *cieInfo](cieCacheSize, hashUint64) + if err != nil { + return err + } + + // Walk the IP search table and dump each FDE found via it + for f := uintptr(0); f < fdeCount; f++ { + ipStart, err := r.ptr(h.tableEnc) + if err != nil { + return err + } + + fdeAddr, err := r.ptr(h.tableEnc) + if err != nil { + return err + } + + if fdeAddr < ehFrameSec.vaddr { + return fmt.Errorf("FDE %#x before section start %#x", + fdeAddr, ehFrameSec.vaddr) + } + + fr := ehFrameSec.reader(fdeAddr-ehFrameSec.vaddr, false) + _, err = fr.parseFDE(ef, ef, ipStart, deltas, hooks, cieCache, true) + if err != nil { + return fmt.Errorf("failed to parse FDE: %v", err) + } + } + + return nil +} + +// 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 { + var err error + + cieCache, err := lru.New[uint64, *cieInfo](cieCacheSize, hashUint64) + if err != nil { + return err + } + + // Walk the section, and process each FDE it contains + 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) + if err != nil && !errors.Is(err, errUnexpectedType) { + return fmt.Errorf("failed to parse FDE %#x: %v", f, err) + } + if entryLen == 0 { + return fmt.Errorf("failed to parse FDE %#x: internal error", f) + } + } + + return nil +} + +func hashUint64(u uint64) uint32 { + return uint32(hash.Uint64(u)) +} + +// 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) + if err != nil { + return fmt.Errorf("failed to get EH sections: %w", err) + } + + if ehFrameSec == nil { + // No eh_frame section being present at all is not an error -- there's simply no data for + // us to parse present. + return nil + } + + if ehFrameHdrSec != nil { + // 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) + } + + // Otherwise, manually walk the FDEs. + return walkFDEs(ef, ef, ehFrameSec, deltas, hooks, false) +} + +// parseDebugFrame parses the .debug_frame DWARF info, extracting stack deltas. +func parseDebugFrame(ef, efCode *pfelf.File, deltas *sdtypes.StackDeltaArray, + hooks ehframeHooks) error { + debugFrameSection := elfRegionFromSection(ef.Section(".debug_frame")) + if debugFrameSection == nil { + return nil + } + + return walkFDEs(ef, efCode, debugFrameSection, deltas, hooks, true) +} diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe_aarch64.go b/libpf/nativeunwind/elfunwindinfo/elfehframe_aarch64.go new file mode 100644 index 00000000..a2ae2aff --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/elfehframe_aarch64.go @@ -0,0 +1,165 @@ +/* + * 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 + +// ARM64 specific code for handling DWARF / stack delta extraction. +// The filename ends with `_aarch64` instead of `_arm64`, so that the code +// can be taken into account regardless of the target build platform. + +import ( + "debug/elf" + "fmt" + + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" +) + +//nolint:deadcode,varcheck +const ( + // Aarch64 ABI + armRegX0 uleb128 = 0 + armRegX1 uleb128 = 1 + armRegX2 uleb128 = 2 + armRegX3 uleb128 = 3 + armRegX4 uleb128 = 4 + armRegX5 uleb128 = 5 + armRegX6 uleb128 = 6 + armRegX7 uleb128 = 7 + armRegX8 uleb128 = 8 + armRegX9 uleb128 = 9 + armRegX10 uleb128 = 10 + armRegX11 uleb128 = 11 + armRegX12 uleb128 = 12 + armRegX13 uleb128 = 13 + armRegX14 uleb128 = 14 + armRegX15 uleb128 = 15 + armRegX16 uleb128 = 16 + armRegX17 uleb128 = 17 + armRegX18 uleb128 = 18 + armRegX19 uleb128 = 19 + armRegX20 uleb128 = 20 + armRegX21 uleb128 = 21 + armRegX22 uleb128 = 22 + armRegX23 uleb128 = 23 + armRegX24 uleb128 = 24 + armRegX25 uleb128 = 25 + armRegX26 uleb128 = 26 + armRegX27 uleb128 = 27 + armRegX28 uleb128 = 28 + armRegFP uleb128 = 29 + armRegLR uleb128 = 30 + armRegSP uleb128 = 31 + armRegPC uleb128 = 32 + + armLastReg uleb128 = iota +) + +// newVMRegsARM initializes the vmRegs structure for aarch64. +func newVMRegsARM() vmRegs { + return vmRegs{ + arch: elf.EM_AARCH64, + cfa: vmReg{arch: elf.EM_AARCH64, reg: regUndefined}, + fp: vmReg{arch: elf.EM_AARCH64, reg: regSame}, + ra: vmReg{arch: elf.EM_AARCH64, reg: regSame}, + } +} + +// getRegNameARM converts register index to a string describing the register +func getRegNameARM(reg uleb128) string { + switch reg { + case armRegFP: + return "fp" + case armRegLR: + return "lr" + case armRegSP: + return "sp" + case armRegPC: + return "pc" + default: + if reg < armLastReg { + return fmt.Sprintf("x%d", reg) + } + return fmt.Sprintf("?%d", reg) + } +} + +// regARM returns the address to ARM specific register in vmRegs +func (regs *vmRegs) regARM(ndx uleb128) *vmReg { + switch ndx { + case armRegFP: + return ®s.fp + case armRegLR: + return ®s.ra + default: + return nil + } +} + +// getUnwindInfo ARM specific part +func (regs *vmRegs) getUnwindInfoARM() sdtypes.UnwindInfo { + // Is CFA valid? + // Not sure if this ever occurs on ARM64, it's been observed that the + // initial CFA opcodes setup CFA to SP. + if regs.cfa.reg == regUndefined { + return sdtypes.UnwindInfoStop + } + + // Undefined RA (aka X30/LR) marks entry point / end-of-stack functions. + if regs.ra.reg == regUndefined { + return sdtypes.UnwindInfoStop + } + + var info sdtypes.UnwindInfo + + // Determine unwind info for stack pointer (CFA) + // For ARM64, the Analyser output indicated only simple (no deref) register + // (usually FP and SP, but sometimes x12 as in qpdldecode) based expressions + // are used for CFA. + switch regs.cfa.reg { + case armRegFP: + info.Opcode = sdtypes.UnwindOpcodeBaseFP + info.Param = int32(regs.cfa.off) + case armRegSP: + info.Opcode = sdtypes.UnwindOpcodeBaseSP + info.Param = int32(regs.cfa.off) + } + + // Determine unwind info for return address + // In order to not increase the EBPF stack deltas map, FP opcode is used + // to hold RA unwinding information. + switch regs.ra.reg { + case regSame: + // for ARM64: + // 1) the link register is loaded with RA prior to the call (one can assume + // it is valid for a sequence of prolog instructions, prior to its value + // being stored into the stack) + // 2) the link register is restored from the stack (one can assume it is + // valid for a sequence of instructions in the function prolog - prior to + // the ret instruction itself) + // thus, the assumption - use UnwindOpcodeBaseLR to instruct native stack + // unwinder to load RA from link register + // This is either prolog or epilog sequence, read RA from link register. + info.FPOpcode = sdtypes.UnwindOpcodeBaseLR + info.FPParam = 0 + case regCFA: + if regs.cfa.off != 0 { + // In ARM64, nothing can be assumed regarding RA location, it is + // simply somewhere on the stack, its detailed location needs to + // be extracted from FDE record. + // In our approach, RA offset part of stack delta always points + // to RA location no matter whether CFA is evaluated with respect + // to SP or FP. + // Use same opcode as for CFA: + info.FPOpcode = info.Opcode + // Convert CFA base to SP / FP base in order to keep + // offset to RA from frame bottom (FP based heuristic). + // CFA offset needs to be added to the one denoting RA location. + info.FPParam = int32(regs.cfa.off) + int32(regs.ra.off) + } + } + + return info +} diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe_test.go b/libpf/nativeunwind/elfunwindinfo/elfehframe_test.go new file mode 100644 index 00000000..71dd020d --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/elfehframe_test.go @@ -0,0 +1,209 @@ +/* + * 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" + "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" +) + +type ehtester struct { + t *testing.T + res map[uintptr]sdtypes.UnwindInfo + found int +} + +func (e *ehtester) fdeHook(cie *cieInfo, fde *fdeInfo) bool { + e.t.Logf("FDE len %d, ciePos %x, ip %x...%x, ipLen %d (enc %x, cf %d, df %d, ra %d)", + fde.len, fde.ciePos, fde.ipStart, fde.ipStart+fde.ipLen, fde.ipLen, + cie.enc, cie.codeAlign, cie.dataAlign, cie.regRA) + e.t.Logf(" LOC CFA rbp ra") + return true +} + +func (e *ehtester) deltaHook(ip uintptr, regs *vmRegs, delta sdtypes.StackDelta) { + e.t.Logf("%016x %-12s %-5s %s", + ip, + regs.cfa.String(), + 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) + } + e.found++ + } +} + +func genDelta(opcode uint8, cfa, rbp int32) sdtypes.UnwindInfo { + res := sdtypes.UnwindInfo{ + Opcode: opcode, + Param: cfa, + } + if rbp != 0 { + res.FPOpcode = sdtypes.UnwindOpcodeBaseCFA + res.FPParam = -rbp + } + return res +} + +func deltaRSP(cfa, rbp int32) sdtypes.UnwindInfo { + return genDelta(sdtypes.UnwindOpcodeBaseSP, cfa, rbp) +} + +func deltaRBP(cfa, rbp int32) sdtypes.UnwindInfo { + return genDelta(sdtypes.UnwindOpcodeBaseFP, cfa, rbp) +} + +func TestEhFrame(t *testing.T) { + tests := map[string]struct { + elfFile string + // Some selected stack delta matches to verify that the ehframe + // machine is working correctly. + res map[uintptr]sdtypes.UnwindInfo + }{ + // test.so is openssl libcrypto.so.1.1's stripped to contain only .eh_frame and + // .eh_frame_hdr. The current ELF is imported from Alpine Linux + // openssl-1.1.1g-r0 package's libcrypto.so.1.1: + // objcopy -j .eh_frame -j .eh_frame_hdr /lib/libcrypto.so.1.1 test.so + "libcrypto": {elfFile: "testdata/test.so", + res: map[uintptr]sdtypes.UnwindInfo{ + 0x07631f: deltaRSP(8, 0), + 0x07a0d4: deltaRSP(160, 24), + 0x07b1ec: deltaRSP(8, 0), + 0x088e72: deltaRSP(64, 48), + 0x0a89d9: deltaRBP(16, 16), + 0x0b2ad4: deltaRBP(8, 24), + 0x1c561f: deltaRSP(2160, 48), + }}, + // schrodinger-libpython3.8.so.1.0 is a stripped version containing only .eh_frame and + // .eh_frame_hdr from /exports/schrodinger/internal/lib/libpython3.8.so.1.0 - see PF-1538. + // objcopy -j .eh_frame -j .eh_frame_hdr /lib/libcrypto.so.1.1 test.so + "schrodinger-libpython": {elfFile: "testdata/schrodinger-libpython3.8.so.1.0", + res: map[uintptr]sdtypes.UnwindInfo{ + 0x6f805: deltaRSP(80, 48), + 0x7077c: deltaRSP(24, 0), + 0x83194: deltaRSP(64, 16), + 0x954b4: deltaRSP(48, 48), + 0xc8b9e: deltaRSP(112, 48), + 0xd2f5e: deltaRSP(56, 48), + 0xf01cf: deltaRSP(24, 24), + 0x1a87b2: deltaRSP(40, 40), + 0x23f555: deltaRSP(56, 48), + }}, + } + + for name, test := range tests { + name := name + 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) + } + 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) + } + }) + } +} + +// 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" + "cie 4": { + debugFrame: true, + expected: &cieInfo{ + dataAlign: sleb128(-4), + codeAlign: uleb128(4), + regRA: uleb128(8), + }, + data: []byte{36, 0, 0, 0, // length + 255, 255, 255, 255, // CIE_id + 4, // version + 0, // augmentation + 4, // address size + 0, // segment size + 4, // code_alignment_factor + 124, // data_alignment_factor + 8, // R8 is the return address + 12, 7, 0, // CFA = [R7]+0 + 8, 0, // R0 not modified + 7, 1, // R1 scratch + 7, 2, // R2 scratch + 7, 3, // R3 scratch + 8, 4, // R4 preserve + 8, 5, // R5 preserve + 8, 6, // R6 preserve + 8, 7, // R7 preserve + 9, 8, 1, // R8 is in R1 + 0, // DW_CFA_nop + 0, // DW_CFA_nop + 0, // DW_CFA_nop + }, + }, + } + + for name, tc := range tests { + name := name + tc := tc + t.Run(name, func(t *testing.T) { + fakeReader := &reader{ + debugFrame: tc.debugFrame, + data: tc.data, + end: uintptr(len(tc.data)), + } + 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) + } + }) + } +} diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe_x86.go b/libpf/nativeunwind/elfunwindinfo/elfehframe_x86.go new file mode 100644 index 00000000..4bec2a4e --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/elfehframe_x86.go @@ -0,0 +1,162 @@ +/* + * 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 + +// x86-64 specific code for handling DWARF / stack delta extraction. +// The filename ends with `_x86` instead of `_amd64`, so that the code +// can be taken into account regardless of the target build platform. + +import ( + "debug/elf" + "fmt" + + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" +) + +//nolint:deadcode,varcheck +const ( + // x86_64 abi (https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf, page 57) + x86RegRAX uleb128 = 0 + x86RegRDX uleb128 = 1 + x86RegRCX uleb128 = 2 + x86RegRBX uleb128 = 3 + x86RegRSI uleb128 = 4 + x86RegRDI uleb128 = 5 + x86RegRBP uleb128 = 6 + x86RegRSP uleb128 = 7 + x86RegR8 uleb128 = 8 + x86RegR9 uleb128 = 9 + x86RegR10 uleb128 = 10 + x86RegR11 uleb128 = 11 + x86RegR12 uleb128 = 12 + x86RegR13 uleb128 = 13 + x86RegR14 uleb128 = 14 + x86RegR15 uleb128 = 15 + x86RegRIP uleb128 = 16 + + x86LastReg uleb128 = iota +) + +// newVMRegsX86 initializes the vmRegs structure for X86_64. +func newVMRegsX86() vmRegs { + return vmRegs{ + arch: elf.EM_X86_64, + cfa: vmReg{arch: elf.EM_X86_64, reg: regUndefined}, + fp: vmReg{arch: elf.EM_X86_64, reg: regUndefined}, + ra: vmReg{arch: elf.EM_X86_64, reg: regUndefined}, + } +} + +// getRegNameX86 converts register index to a string describing x86 register +func getRegNameX86(reg uleb128) string { + switch reg { + case x86RegRAX: + return "rax" + case x86RegRDX: + return "rdx" + case x86RegRCX: + return "rcx" + case x86RegRBX: + return "rbx" + case x86RegRSI: + return "rsi" + case x86RegRDI: + return "rdi" + case x86RegRBP: + return "rbp" + case x86RegRSP: + return "rsp" + default: + if reg < x86LastReg { + return fmt.Sprintf("r%d", reg) + } + return fmt.Sprintf("?%d", reg) + } +} + +// regX86 returns the address to x86 specific register in vmRegs +func (regs *vmRegs) regX86(ndx uleb128) *vmReg { + switch ndx { + case x86RegRBP: + return ®s.fp + case x86RegRIP: + return ®s.ra + default: + return nil + } +} + +// getUnwindInfo x86 specific part +func (regs *vmRegs) getUnwindInfoX86() sdtypes.UnwindInfo { + // Is CFA and RIP (return address) valid? + if regs.cfa.reg == regUndefined || regs.ra.reg == regUndefined { + return sdtypes.UnwindInfoStop + } + + // Is RA popped out from stack? + if regs.ra.reg == regCFA && regs.cfa.reg == x86RegRSP && regs.cfa.off+regs.ra.off < 0 { + // It depends on context if this is INVALID or STOP. As this catch the musl + // thread start __clone function, treat this as STOP. Seeing the INVALID + // condition in samples is statistically unlikely. + return sdtypes.UnwindInfoStop + } + + // The CFI allows having Return Address (RA) be recoverable via an expression, + // but the eBPF currently supports the ABI standard RA=CFA-8 only. Verify that + // we are not in any weird hand woven assembly which is not supported. + if regs.ra.reg != regCFA || regs.ra.off != -8 { + return sdtypes.UnwindInfoInvalid + } + + info := sdtypes.UnwindInfo{} + + // Determine unwind info for frame pointer + switch regs.fp.reg { + case regCFA: + // Check that RBP is between CFA and stack top + if regs.cfa.reg != x86RegRSP || (regs.fp.off < 0 && regs.fp.off >= -regs.cfa.off) { + info.FPOpcode = sdtypes.UnwindOpcodeBaseCFA + info.FPParam = int32(regs.fp.off) + } + case regExprReg: + // expression: RBP+offrbp + if r, _, offrbp, _ := splitOff(regs.fp.off); uleb128(r) == x86RegRBP { + info.FPOpcode = sdtypes.UnwindOpcodeBaseFP + info.FPParam = int32(offrbp) + } + } + + // Determine unwind info for stack pointer + switch regs.cfa.reg { + case x86RegRBP: + info.Opcode = sdtypes.UnwindOpcodeBaseFP + info.Param = int32(regs.cfa.off) + case x86RegRSP: + if regs.cfa.off != 0 { + info.Opcode = sdtypes.UnwindOpcodeBaseSP + info.Param = int32(regs.cfa.off) + } + case regExprPLT: + info.Opcode = sdtypes.UnwindOpcodeCommand + info.Param = sdtypes.UnwindCommandPLT + case regExprRegDeref: + reg, _, off, off2 := splitOff(regs.cfa.off) + if param, ok := sdtypes.PackDerefParam(int32(off), int32(off2)); ok { + switch uleb128(reg) { + case x86RegRBP: + // GCC SSE vectorized functions + info.Opcode = sdtypes.UnwindOpcodeBaseFP | sdtypes.UnwindOpcodeFlagDeref + info.Param = param + case x86RegRSP: + // OpenSSL assembly using SSE/AVX + info.Opcode = sdtypes.UnwindOpcodeBaseSP | sdtypes.UnwindOpcodeFlagDeref + info.Param = param + } + } + } + return info +} diff --git a/libpf/nativeunwind/elfunwindinfo/elfgopclntab.go b/libpf/nativeunwind/elfunwindinfo/elfgopclntab.go new file mode 100644 index 00000000..7581aa5f --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/elfgopclntab.go @@ -0,0 +1,681 @@ +/* + * 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. + */ + +// This implements Go 1.2+ .pclntab symbol parsing as defined +// in http://golang.org/s/go12symtab. The Golang runtime implementation of +// this is in go/src/runtime/symtab.go, but unfortunately it is not exported. + +package elfunwindinfo + +import ( + "bytes" + "debug/elf" + "fmt" + "unsafe" + + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + log "github.com/sirupsen/logrus" +) + +// Go runtime functions for which we should not attempt to unwind further +var goFunctionsStopDelta = map[string]*sdtypes.UnwindInfo{ + "runtime.mstart": &sdtypes.UnwindInfoStop, // topmost for the go runtime main stacks + "runtime.goexit": &sdtypes.UnwindInfoStop, // return address in all goroutine stacks + + // stack switch functions that would need special handling for further unwinding. + // See PF-1101. + "runtime.mcall": &sdtypes.UnwindInfoStop, + "runtime.systemstack": &sdtypes.UnwindInfoStop, + + // signal return frame + "runtime.sigreturn": &sdtypes.UnwindInfoSignal, +} + +const ( + // maximum pclntab (or rodata segment) size to inspect. The .gopclntab is + // often huge. Host agent binaries have about 32M .rodata, so allow for more. + maxBytesGoPclntab = 128 * 1024 * 1024 + + // pclntabHeader magic identifying Go version + magicGo1_2 = 0xfffffffb + magicGo1_16 = 0xfffffffa + magicGo1_18 = 0xfffffff0 + magicGo1_20 = 0xfffffff1 +) + +// pclntabHeader is the Golang pclntab header structure +// +//nolint:structcheck +type pclntabHeader struct { + // magic is one of the magicGo1_xx constants identifying the version + magic uint32 + // pad is unused and is needed for alignment + pad uint16 + // quantum is the CPU instruction size alignment (e.g. 1 for x86, 4 for arm) + quantum uint8 + // ptrSize is the CPU pointer size in bytes + ptrSize uint8 + // numFuncs is the number of function definitions to follow + numFuncs uint64 +} + +// pclntabHeader116 is the Golang pclntab header structure starting Go 1.16 +// structural definition of this is found in go/src/runtime/symtab.go as pcHeader +// +//nolint:structcheck +type pclntabHeader116 struct { + pclntabHeader + nfiles uint + funcnameOffset uintptr + cuOffset uintptr + filetabOffset uintptr + pctabOffset uintptr + pclnOffset uintptr +} + +// pclntabHeader118 is the Golang pclntab header structure starting Go 1.18 +// structural definition of this is found in go/src/runtime/symtab.go as pcHeader +// +//nolint:structcheck +type pclntabHeader118 struct { + pclntabHeader + nfiles uint + textStart uintptr + funcnameOffset uintptr + cuOffset uintptr + filetabOffset uintptr + pctabOffset uintptr + pclnOffset uintptr +} + +// pclntabFuncMap is the Golang function symbol table map entry +// +//nolint:structcheck +type pclntabFuncMap struct { + pc uint64 + funcOff uint64 +} + +// pclntabFunc is the Golang function definition (struct _func in the spec) as before Go 1.18. +// +//nolint:structcheck +type pclntabFunc struct { + startPc uint64 + nameOff, argsSize, frameSize int32 + pcspOff, pcfileOff, pclnOff int32 + nfuncData, npcData int32 +} + +// pclntabFunc118 is the Golang function definition (struct _func in the spec) +// starting with Go 1.18. +// see: go/src/runtime/runtime2.go (struct _func) +// +//nolint:structcheck +type pclntabFunc118 struct { + entryoff uint32 // start pc, as offset from pcHeader.textStart + nameOff, argsSize, frameSize int32 + pcspOff, pcfileOff, pclnOff int32 + nfuncData, npcData int32 +} + +// pcval describes a Program Counter (pc) and a value (val) associated with it, +// as well as the slice containing the full pcval data. The meaning of the value +// depends on which table is being processed. It can signify the Stack Delta in +// bytes, the source filename index, or the source line number. +type pcval struct { + ptr []byte + pcStart uint + pcEnd uint + val int32 + quantum uint8 +} + +// PclntabHeaderSize returns the minimal pclntab header size. +func PclntabHeaderSize() int { + return int(unsafe.Sizeof(pclntabHeader{})) +} + +// IsGo118orNewer returns true if magic matches with the Go 1.18 or newer. +func IsGo118orNewer(magic uint32) bool { + return magic == magicGo1_18 || magic == magicGo1_20 +} + +// pclntabHeaderSignature returns a byte slice that can be +// used to verify if some bytes represent a valid pclntab header. +func pclntabHeaderSignature(arch elf.Machine) []byte { + var quantum byte + + switch arch { + case elf.EM_X86_64: + quantum = 0x1 + case elf.EM_AARCH64: + quantum = 0x4 + } + + // - the first byte is ignored and not included in this signature + // as it is different per Go version (see magicGo1_XX) + // - next three bytes are 0xff (shared on magicGo1_XX) + // - pad is zero (two bytes) + // - quantum depends on the architecture + // - ptrSize is 8 for 64 bit systems (arm64 and amd64) + + return []byte{0xff, 0xff, 0xff, 0x00, 0x00, quantum, 0x08} +} + +func newPcval(data []byte, pc uint, quantum uint8) pcval { + p := pcval{ + ptr: data, + pcEnd: pc, + val: -1, + quantum: quantum, + } + p.step() + return p +} + +// getInt reads one zig-zag encoded integer +func (p *pcval) getInt() uint32 { + var v, shift uint32 + for { + if len(p.ptr) == 0 { + return 0 + } + b := p.ptr[0] + p.ptr = p.ptr[1:] + v |= (uint32(b) & 0x7F) << shift + if b&0x80 == 0 { + break + } + shift += 7 + } + return v +} + +// step executes one line of the pcval table. Returns true on success. +func (p *pcval) step() bool { + if len(p.ptr) == 0 || p.ptr[0] == 0 { + return false + } + p.pcStart = p.pcEnd + d := p.getInt() + if d&1 != 0 { + d = ^(d >> 1) + } else { + d >>= 1 + } + p.val += int32(d) + p.pcEnd += uint(p.getInt()) * uint(p.quantum) + return true +} + +// getInt32 gets a 32-bit integer from the data slice at offset with bounds checking +func getInt32(data []byte, offset int) int { + if offset < 0 || offset+4 > len(data) { + return -1 + } + return int(*(*int32)(unsafe.Pointer(&data[offset]))) +} + +// getString returns a zero terminated string from the data slice at given offset as []byte +func getString(data []byte, offset int) []byte { + if offset < 0 || offset > len(data) { + return nil + } + zeroIdx := bytes.IndexByte(data[offset:], 0) + if zeroIdx < 0 { + return nil + } + return data[offset : offset+zeroIdx] +} + +const ( + strategyUnknown = iota + strategyFramePointer + strategyDeltasWithoutRBP + strategyDeltasWithRBP +) + +// noFPSourceSuffixes lists the go runtime source files that call assembly code +// which trashes RBP. These source files need to use explicit SP delta so that +// RBP can be recovered, and be then further used for frame pointer based unwinding. +// This lists the most notable problem cases from Go runtime. +// TODO(tteras) Go Runtime files calling internal.bytealg.Index* may need to be added here. +var noFPSourceSuffixes = [][]byte{ + []byte("/src/crypto/sha1/sha1.go"), + []byte("/src/crypto/sha256/sha256.go"), + []byte("/src/crypto/sha512/sha512.go"), + []byte("/src/crypto/elliptic/p256_asm.go"), + []byte("golang.org/x/crypto/curve25519/curve25519_amd64.go"), + []byte("golang.org/x/crypto/chacha20poly1305/chacha20poly1305_amd64.go"), +} + +// getSourceFileStrategy categorizes sourceFile's unwinding strategy based on its name +func getSourceFileStrategy(arch elf.Machine, sourceFile []byte) int { + switch arch { + case elf.EM_X86_64: + // Most of the assembly code needs explicit SP delta as they do not + // create stack frame. Do not recover RBP as it is not modified. + if bytes.HasSuffix(sourceFile, []byte(".s")) { + return strategyDeltasWithoutRBP + } + + // Check for the Go source files needing SP delta unwinding to recover RBP + for _, suffix := range noFPSourceSuffixes { + if bytes.HasSuffix(sourceFile, suffix) { + return strategyDeltasWithRBP + } + } + case elf.EM_AARCH64: + // Assume all code has frame pointers as the code generated by Golang compiler + // for ARM64 supports frame pointers even for asm code. Frame pointers + // get omitted only for leaf, no arg functions. + return strategyFramePointer + } + + // Use frame pointer for others + return strategyFramePointer +} + +// SearchGoPclntab uses heuristic to find the gopclntab from RO data. +func SearchGoPclntab(ef *pfelf.File) ([]byte, error) { + // The sections headers are not available for coredump testing, because they are + // not inside any PT_LOAD segment. And in the case ofwhere they might be available + // because of alignment they are likely not usable, e.g. the musl C-library will + // reuse that area via malloc. + // + // Go does emit "runtime.pclntab" and "runtime.epclntab" symbols of the .gopclntab + // too, but these are not dynamic, and we'd need the full symbol table from + // the .symtab section to get them. This means that these symbols are not present + // in the symbol hash table, nor in the .dynsym symbol table available via dynamic + // section. + // + // So the only thing that works for ELF files inside core dump files is to use + // a heuristic to find the .gopclntab from the RO data segment based on its header. + + signature := pclntabHeaderSignature(ef.Machine) + + for i := range ef.Progs { + p := &ef.Progs[i] + // Search for the .rodata (read-only) and .data.rel.ro (read-write which gets + // turned into read-only after relocations handling via GNU_RELRO header). + if p.Type != elf.PT_LOAD || p.Flags&elf.PF_X == elf.PF_X || p.Flags&elf.PF_R != elf.PF_R { + continue + } + + var data []byte + var err error + if data, err = p.Data(maxBytesGoPclntab); err != nil { + return nil, err + } + + for i := 1; i < len(data)-PclntabHeaderSize(); i += 8 { + // Search for something looking like a valid pclntabHeader header + // Ignore the first byte on bytes.Index (differs on magicGo1_XXX) + n := bytes.Index(data[i:], signature) + if n < 0 { + break + } + i += n - 1 + + // Check the 'magic' against supported list, and if valid, use this + // location as the .gopclntab base. Otherwise, continue just search + // for next candidate location. + hdr := (*pclntabHeader)(unsafe.Pointer(&data[i])) + switch hdr.magic { + case magicGo1_20, magicGo1_18, magicGo1_16, magicGo1_2: + return data[i:], nil + } + } + } + + return nil, nil +} + +// 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 { + var err error + var data []byte + + if ef.InsideCore { + // Section tables not available. Use heuristic. Ignore errors as + // this might not be a Go binary. + data, _ = SearchGoPclntab(ef) + } else if s := ef.Section(".gopclntab"); s != nil { + // Load the .gopclntab via section if available. + if data, err = s.Data(maxBytesGoPclntab); err != nil { + return fmt.Errorf("failed to load .gopclntab section: %v", err) + } + } else if s := ef.Section(".go.buildinfo"); s != nil { + // This looks like Go binary. Lookup the runtime.pclntab symbols, + // as the .gopclntab section is not available on PIE binaries. + // A full symbol table read is needed as these are not dynamic symbols. + // Consequently these symbols might be unavailable on a stripped binary. + symtab, err := ef.ReadSymbols() + if err != nil { + // It seems the Go binary was stripped. So we use the heuristic approach + // to get the stack deltas. + if data, err = SearchGoPclntab(ef); err != nil { + return fmt.Errorf("failed to search .gopclntab: %v", err) + } + } else { + start, err := symtab.LookupSymbolAddress("runtime.pclntab") + if err != nil { + return fmt.Errorf("failed to load .gopclntab via symbols: %v", err) + } + end, err := symtab.LookupSymbolAddress("runtime.epclntab") + if err != nil { + return fmt.Errorf("failed to load .gopclntab via symbols: %v", err) + } + if start >= end { + return fmt.Errorf("invalid .gopclntab symbols: %v-%v", start, end) + } + data = make([]byte, end-start) + if _, err := ef.ReadVirtualMemory(data, int64(start)); err != nil { + return fmt.Errorf("failed to load .gopclntab via symbols: %v", err) + } + } + } + if data == nil { + return nil + } + + var textStart uintptr + hdrSize := uintptr(PclntabHeaderSize()) + mapSize := unsafe.Sizeof(pclntabFuncMap{}) + funSize := unsafe.Sizeof(pclntabFunc{}) + dataLen := uintptr(len(data)) + if dataLen < hdrSize { + return fmt.Errorf(".gopclntab is too short (%v)", len(data)) + } + + var functab, funcdata, funcnametab, filetab, pctab, cutab []byte + + hdr := (*pclntabHeader)(unsafe.Pointer(&data[0])) + fieldSize := uintptr(hdr.ptrSize) + switch hdr.magic { + case magicGo1_2: + functabEnd := int(hdrSize + uintptr(hdr.numFuncs)*mapSize + uintptr(hdr.ptrSize)) + filetabOffset := getInt32(data, functabEnd) + numSourceFiles := getInt32(data, filetabOffset) + if filetabOffset == 0 || numSourceFiles == 0 { + return fmt.Errorf(".gopclntab corrupt (filetab 0x%x, nfiles %d)", + filetabOffset, numSourceFiles) + } + functab = data[hdrSize:filetabOffset] + cutab = data[filetabOffset:] + pctab = data + funcnametab = data + funcdata = data + filetab = data + case magicGo1_16: + hdrSize = unsafe.Sizeof(pclntabHeader116{}) + if dataLen < hdrSize { + return fmt.Errorf(".gopclntab is too short (%v)", len(data)) + } + hdr116 := (*pclntabHeader116)(unsafe.Pointer(&data[0])) + if dataLen < hdr116.funcnameOffset || dataLen < hdr116.cuOffset || + dataLen < hdr116.filetabOffset || dataLen < hdr116.pctabOffset || + dataLen < hdr116.pclnOffset { + return fmt.Errorf(".gopclntab is corrupt (%x, %x, %x, %x, %x)", + hdr116.funcnameOffset, hdr116.cuOffset, + hdr116.filetabOffset, hdr116.pctabOffset, + hdr116.pclnOffset) + } + funcnametab = data[hdr116.funcnameOffset:] + cutab = data[hdr116.cuOffset:] + filetab = data[hdr116.filetabOffset:] + pctab = data[hdr116.pctabOffset:] + functab = data[hdr116.pclnOffset:] + funcdata = functab + case magicGo1_18, magicGo1_20: + hdrSize = unsafe.Sizeof(pclntabHeader118{}) + if dataLen < hdrSize { + return fmt.Errorf(".gopclntab is too short (%v)", dataLen) + } + hdr118 := (*pclntabHeader118)(unsafe.Pointer(&data[0])) + if dataLen < hdr118.funcnameOffset || dataLen < hdr118.cuOffset || + dataLen < hdr118.filetabOffset || dataLen < hdr118.pctabOffset || + dataLen < hdr118.pclnOffset { + return fmt.Errorf(".gopclntab is corrupt (%x, %x, %x, %x, %x)", + hdr118.funcnameOffset, hdr118.cuOffset, + hdr118.filetabOffset, hdr118.pctabOffset, + hdr118.pclnOffset) + } + funcnametab = data[hdr118.funcnameOffset:] + cutab = data[hdr118.cuOffset:] + filetab = data[hdr118.filetabOffset:] + pctab = data[hdr118.pctabOffset:] + functab = data[hdr118.pclnOffset:] + funcdata = functab + textStart = hdr118.textStart + funSize = unsafe.Sizeof(pclntabFunc118{}) + // With the change of the type of the first field of _func in Go 1.18, this + // value is now hard coded. + // + // nolint:lll + // See https://github.com/golang/go/blob/6df0957060b1315db4fd6a359eefc3ee92fcc198/src/debug/gosym/pclntab.go#L376-L382 + fieldSize = uintptr(4) + mapSize = fieldSize * 2 + default: + return fmt.Errorf(".gopclntab format (0x%x) not supported", hdr.magic) + } + if hdr.pad != 0 || hdr.ptrSize != 8 { + return fmt.Errorf(".gopclntab header: %x, %x", hdr.pad, hdr.ptrSize) + } + + // Go uses frame-pointers by default since Go 1.7, but unfortunately + // it is not necessarily available when in code from non-Golang source + // files, such as the assembly, of the Go runtime. + // Since Golang binaries are huge statically compiled executables and + // would fill up our precious kernel delta maps fast, the strategy is to + // create deltastack maps for non-Go source files only, and otherwise + // cover the vast majority with "use frame pointer" stack delta. + sourceStrategy := make(map[int]int) + + // Get target machine architecture for the ELF file + arch := ef.Machine + + fmap := &pclntabFuncMap{} + fun := &pclntabFunc{} + // Iterate the golang PC to function lookup table (sorted by PC) + for i := uint64(0); i < hdr.numFuncs; i++ { + if IsGo118orNewer(hdr.magic) { + // nolint:lll + // See: https://github.com/golang/go/blob/6df0957060b1315db4fd6a359eefc3ee92fcc198/src/debug/gosym/pclntab.go#L401-L413 + *fmap = pclntabFuncMap{} + funcIdx := uintptr(i) * 2 * fieldSize + fmap.pc = uint64(*(*uint32)(unsafe.Pointer(&functab[funcIdx]))) + fmap.funcOff = uint64(*(*uint32)(unsafe.Pointer(&functab[funcIdx+fieldSize]))) + fmap.pc += uint64(textStart) + } else { + fmap = (*pclntabFuncMap)(unsafe.Pointer(&functab[uintptr(i)*mapSize])) + } + // Get the function data + if uintptr(len(funcdata)) < uintptr(fmap.funcOff)+funSize { + return fmt.Errorf(".gopclntab func %v descriptor is invalid", i) + } + if IsGo118orNewer(hdr.magic) { + tmp := (*pclntabFunc118)(unsafe.Pointer(&funcdata[fmap.funcOff])) + *fun = pclntabFunc{ + startPc: uint64(textStart) + uint64(tmp.entryoff), + nameOff: tmp.nameOff, + argsSize: tmp.argsSize, + frameSize: tmp.argsSize, + pcspOff: tmp.pcspOff, + pcfileOff: tmp.pcfileOff, + pclnOff: tmp.pclnOff, + nfuncData: tmp.nfuncData, + npcData: tmp.npcData, + } + } else { + fun = (*pclntabFunc)(unsafe.Pointer(&funcdata[fmap.funcOff])) + } + // First, check for functions with special handling. + funcName := getString(funcnametab, int(fun.nameOff)) + if info, found := goFunctionsStopDelta[string(funcName)]; found { + deltas.Add(sdtypes.StackDelta{ + Address: fun.startPc, + Info: *info, + }) + continue + } + + // Use source file to determine strategy if possible, and default + // to using frame pointers in the unlikely case of no file info + strategy := strategyFramePointer + if fun.pcfileOff != 0 { + p := newPcval(pctab[fun.pcfileOff:], uint(fun.startPc), hdr.quantum) + fileIndex := int(p.val) + if hdr.magic == magicGo1_16 || IsGo118orNewer(hdr.magic) { + fileIndex += int(fun.npcData) + } + + // Determine strategy + strategy = sourceStrategy[fileIndex] + if strategy == strategyUnknown { + sourceFile := getString(filetab, getInt32(cutab, 4*fileIndex)) + strategy = getSourceFileStrategy(arch, sourceFile) + sourceStrategy[fileIndex] = strategy + } + } + + switch arch { + case elf.EM_X86_64: + if err := parseX86pclntabFunc(deltas, fun, dataLen, pctab, strategy, i, + hdr.quantum); err != nil { + return err + } + case elf.EM_AARCH64: + if err := parseArm64pclntabFunc(deltas, fun, dataLen, pctab, i, + hdr.quantum); err != nil { + return err + } + } + } + + // Filter out .gopclntab info from other sources + 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 + // 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 + } else { + f.start = *(*uintptr)(unsafe.Pointer(&functab[0])) + f.end = *(*uintptr)(unsafe.Pointer(&functab[uintptr(hdr.numFuncs)*mapSize])) + } + f.golangFrames = true + + // Add end of code indicator + deltas.Add(sdtypes.StackDelta{ + Address: uint64(f.end), + Info: sdtypes.UnwindInfoInvalid, + }) + + return nil +} + +// parseX86pclntabFunc extracts interval information from x86_64 based pclntabFunc. +func parseX86pclntabFunc(deltas *sdtypes.StackDeltaArray, fun *pclntabFunc, dataLen uintptr, + pctab []byte, strategy int, i uint64, quantum uint8) error { + switch { + case strategy == strategyFramePointer: + // Use stack frame-pointer delta + deltas.Add(sdtypes.StackDelta{ + Address: fun.startPc, + Info: sdtypes.UnwindInfoFramePointer, + }) + return nil + case fun.pcspOff != 0: + // Generate stack deltas as the information is available + if dataLen < uintptr(fun.pcspOff) { + return fmt.Errorf(".gopclntab func %v pcscOff (%d) is invalid", + i, fun.pcspOff) + } + + p := newPcval(pctab[fun.pcspOff:], uint(fun.startPc), quantum) + hints := sdtypes.UnwindHintKeep + for ok := true; ok; ok = p.step() { + info := sdtypes.UnwindInfo{ + Opcode: sdtypes.UnwindOpcodeBaseSP, + Param: p.val + 8, + } + if strategy == strategyDeltasWithRBP && info.Param >= 16 { + info.FPOpcode = sdtypes.UnwindOpcodeBaseCFA + info.FPParam = -16 + } + deltas.Add(sdtypes.StackDelta{ + Address: uint64(p.pcStart), + Hints: hints, + Info: info, + }) + hints = sdtypes.UnwindHintNone + } + } + log.Debugf("Unhandled .gopclntab func at %d", i) + return nil +} + +// parseArm64pclntabFunc extracts interval information from ARM64 based pclntabFunc. +func parseArm64pclntabFunc(deltas *sdtypes.StackDeltaArray, fun *pclntabFunc, + dataLen uintptr, pctab []byte, i uint64, quantum uint8) error { + if fun.pcspOff == 0 { + // Some CGO functions don't have PCSP info: skip them. + return nil + } + if dataLen < uintptr(fun.pcspOff) { + return fmt.Errorf(".gopclntab func %v pcspOff = %d is invalid", i, fun.pcspOff) + } + + // On ARM64, frame pointers are not properly kept when the Go runtime copies the stack during + // `runtime.morestack` calls: all old frame pointers are set to 0. + // + // https://github.com/golang/go/blob/c318f191/src/runtime/stack.go#L676 + // + // We thus need to unwind with stack delta offsets. + + hint := sdtypes.UnwindHintKeep + p := newPcval(pctab[fun.pcspOff:], uint(fun.startPc), quantum) + for ok := true; ok; ok = p.step() { + var info sdtypes.UnwindInfo + if p.val == 0 { + // Return instruction, function prologue or leaf function body: unwind via LR. + info = sdtypes.UnwindInfo{ + Opcode: sdtypes.UnwindOpcodeBaseSP, + Param: 0, + FPOpcode: sdtypes.UnwindOpcodeBaseLR, + FPParam: 0, + } + } else { + // Regular basic block in the function body: unwind via SP. + info = sdtypes.UnwindInfo{ + // Unwind via SP offset. + Opcode: sdtypes.UnwindOpcodeBaseSP, + Param: p.val, + // On ARM64, the previous LR value is stored to top-of-stack. + FPOpcode: sdtypes.UnwindOpcodeBaseSP, + FPParam: 0, + } + } + + deltas.Add(sdtypes.StackDelta{ + Address: uint64(p.pcStart), + Hints: hint, + Info: info, + }) + + hint = sdtypes.UnwindHintNone + } + + return nil +} diff --git a/libpf/nativeunwind/elfunwindinfo/elfgopclntab_test.go b/libpf/nativeunwind/elfunwindinfo/elfgopclntab_test.go new file mode 100644 index 00000000..0dbddec1 --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/elfgopclntab_test.go @@ -0,0 +1,108 @@ +/* + * 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 ( + "debug/elf" + "testing" + + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" +) + +// Go 1.2 spec Appendix: PC-Value Table Encoding example +func TestPcval(t *testing.T) { + res := []struct { + val int32 + pc uint + }{ + {0, 0x2019}, + {32, 0x206b}, + {40, 0x206d}, + {48, 0x2073}, + {40, 0x2074}, + {32, 0x209b}, + {0, 0x209c}, + } + data := []byte{ + 0x02, 0x19, 0x40, 0x52, 0x10, 0x02, 0x10, 0x06, + 0x0f, 0x01, 0x0f, 0x27, 0x3f, 0x01, 0x00} + p := newPcval(data, 0x2000, 1) + 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) + } + i++ + } + if i != len(res) { + t.Fatalf("Table not decoded in full") + } +} + +// Pcval with sequence that would result in out-of-bound read +func TestPcvalInvalid(_ *testing.T) { + data := []byte{0x81} + p := newPcval(data, 0x2000, 1) + for p.step() { + } +} + +// Some strategy tests +func TestGoStrategy(t *testing.T) { + res := []struct { + file string + strategy int + }{ + {"foo.go", strategyFramePointer}, + {"foo.s", strategyDeltasWithoutRBP}, + {"go/src/crypto/elliptic/p256_asm.go", strategyDeltasWithRBP}, + } + 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) + } + } +} + +func TestParseGoPclntab(t *testing.T) { + tests := map[string]struct { + elfFile string + }{ + // helloworld is a very basic Go binary without special build flags. + "regular Go binary": {elfFile: "testdata/helloworld"}, + "regular ARM64 Go binary": {elfFile: "testdata/helloworld.arm64"}, + // helloworld.pie is a Go binary that is build with PIE enabled. + "PIE Go binary": {elfFile: "testdata/helloworld.pie"}, + // helloworld.stripped.pie is a Go binary that is build with PIE enabled and all debug + // information stripped. + "stripped PIE Go binary": {elfFile: "testdata/helloworld.stripped.pie"}, + } + + for name, test := range tests { + 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") + } + }) + } +} diff --git a/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction.go b/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction.go new file mode 100644 index 00000000..774f2b7c --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction.go @@ -0,0 +1,168 @@ +/* + * 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 ( + "fmt" + "sort" + + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" +) + +const ( + // Some DSOs have few limited .eh_frame FDEs (e.g. PLT), and additional + // FDEs are in .debug_frame or external debug file. This controls how many + // intervals are needed to not follow .gnu_debuglink. + numIntervalsToOmitDebugLink = 20 +) + +// extractionFilter is used to filter in .eh_frame data when a better source +// is available (.gopclntab). +type extractionFilter struct { + // start and end contains the virtual address block of code which + // should be excluded from .eh_frame extraction. + start, end uintptr + + // ehFrames is true if .eh_frame stack deltas are found + ehFrames bool + + // golangFrames is true if .gopclntab stack deltas are found + golangFrames bool + + // unsortedFrames is set if stack deltas from unsorted source are found + unsortedFrames bool +} + +var _ ehframeHooks = &extractionFilter{} + +// fdeHook filters out .eh_frame data that is superseded by .gopclntab data +func (f *extractionFilter) fdeHook(_ *cieInfo, fde *fdeInfo) bool { + if !fde.sorted { + // Seems .debug_frame sometimes has broken FDEs for zero address + if fde.ipStart == 0 { + return false + } + f.unsortedFrames = true + } + // Parse functions outside the gopclntab area + if fde.ipStart < f.start || fde.ipStart > f.end { + // This is here to set the flag only when we have collected at least + // one stack delta from the relevant source. + f.ehFrames = true + return true + } + return false +} + +// deltaHook is a stub to satisfy ehframeHooks interface +func (f *extractionFilter) deltaHook(uintptr, *vmRegs, sdtypes.StackDelta) { +} + +func extractDebugDeltas(elfFile *pfelf.File, elfRef *pfelf.Reference, + deltas *sdtypes.StackDeltaArray, filter *extractionFilter) 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) + if debugELF != nil { + err = parseDebugFrame(debugELF, elfFile, deltas, filter) + debugELF.Close() + } + return err +} + +// 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 { + elfRef := pfelf.NewReference(filename, pfelf.SystemOpener) + defer elfRef.Close() + return ExtractELF(elfRef, interval) +} + +// ExtractELF takes a pfelf.Reference and provides the stack delta +// intervals for it in the interval parameter. +func ExtractELF(elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { + elfFile, err := elfRef.GetELF() + if err != nil { + return err + } + + // Parse the stack deltas from the ELF + deltas := sdtypes.StackDeltaArray{} + filter := &extractionFilter{} + + if err = parseGoPclntab(elfFile, &deltas, filter); err != nil { + return fmt.Errorf("failure to parse golang stack deltas: %v", err) + } + if err = parseEHFrame(elfFile, &deltas, filter); err != nil { + return fmt.Errorf("failure to parse eh_frame stack deltas: %v", err) + } + if err = parseDebugFrame(elfFile, elfFile, &deltas, filter); 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 { + return fmt.Errorf("failure to parse debug stack deltas: %v", err) + } + } + + // If multiple sources were merged, sort them. + if filter.unsortedFrames || (filter.ehFrames && filter.golangFrames) { + sort.Slice(deltas, func(i, j int) bool { + if deltas[i].Address != deltas[j].Address { + return deltas[i].Address < deltas[j].Address + } + // Make sure that the potential duplicate stop delta is sorted + // after the real delta. + return deltas[i].Info.Opcode < deltas[j].Info.Opcode + }) + + maxDelta := 0 + for i := 0; i < len(deltas); i++ { + delta := &deltas[i] + if maxDelta > 0 { + // This duplicates the logic from StackDeltaArray.Add() + // to remove duplicate and redundant stack deltas. + prev := &deltas[maxDelta-1] + if prev.Hints&sdtypes.UnwindHintGap != 0 && + prev.Address+sdtypes.MinimumGap >= delta.Address { + // The previous opcode is end-of-function marker, and + // the gap is not large. Reduce deltas by overwriting it. + if maxDelta <= 1 || deltas[maxDelta-2].Info != delta.Info { + *prev = *delta + continue + } + // The delta before end-of-function marker is same as + // what is being inserted now. Overwrite that. + prev = &deltas[maxDelta-2] + maxDelta-- + } + if prev.Info == delta.Info { + prev.Hints |= delta.Hints & sdtypes.UnwindHintKeep + continue + } + if prev.Address == delta.Address { + *prev = *delta + continue + } + } + deltas[maxDelta] = *delta + maxDelta++ + } + deltas = deltas[:maxDelta] + } + + *interval = sdtypes.IntervalData{ + Deltas: deltas, + } + return nil +} diff --git a/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go b/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go new file mode 100644 index 00000000..7bd42af6 --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go @@ -0,0 +1,172 @@ +/* + * 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 ( + "encoding/base64" + "os" + "testing" + + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + "github.com/google/go-cmp/cmp" +) + +// Base64-encoded data from /usr/bin/volname on a stock debian box, the smallest +// 64-bit executable on my system (about 6k). +var usrBinVolname = `f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAA8AkAAAAAAABAAAAAAAAAADgRAAAAAAAAAAAAAEAAOAAJ +AEAAGwAaAAYAAAAFAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAA+AEAAAAAAAD4AQAAAAAAAAgA +AAAAAAAAAwAAAAQAAAA4AgAAAAAAADgCAAAAAAAAOAIAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA +AAAAAAABAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFQNAAAAAAAAVA0AAAAAAAAAACAA +AAAAAAEAAAAGAAAAgA0AAAAAAACADSAAAAAAAIANIAAAAAAAkAIAAAAAAACwAgAAAAAAAAAAIAAA +AAAAAgAAAAYAAACYDQAAAAAAAJgNIAAAAAAAmA0gAAAAAADAAQAAAAAAAMABAAAAAAAACAAAAAAA +AAAEAAAABAAAAFQCAAAAAAAAVAIAAAAAAABUAgAAAAAAAEQAAAAAAAAARAAAAAAAAAAEAAAAAAAA +AFDldGQEAAAA+AsAAAAAAAD4CwAAAAAAAPgLAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAQAAAAAAAAA +UeV0ZAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABS +5XRkBAAAAIANAAAAAAAAgA0gAAAAAACADSAAAAAAAIACAAAAAAAAgAIAAAAAAAABAAAAAAAAAC9s +aWI2NC9sZC1saW51eC14ODYtNjQuc28uMgAEAAAAEAAAAAEAAABHTlUAAAAAAAIAAAAGAAAAIAAA +AAQAAAAUAAAAAwAAAEdOVQCSX5P2bs4LXU0AZhU77QH4cIow5gIAAAATAAAAAQAAAAYAAAAAAQAA +AAAAAgAAAAATAAAAOfKLHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACeAAAAIAAAAAAAAAAA +AAAAAAAAAAAAAACBAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAB9AAAAEgAAAAAAAAAAAAAAAAAAAAAA +AAAuAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAEgAAAAAAAAAAAAAAAAAAAAAAAABcAAAAEgAA +AAAAAAAAAAAAAAAAAAAAAABJAAAAEgAAAAAAAAAAAAAAAAAAAAAAAACMAAAAEgAAAAAAAAAAAAAA +AAAAAAAAAAC6AAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAdAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAL +AAAAEgAAAAAAAAAAAAAAAAAAAAAAAABpAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAnAAAAEgAAAAAA +AAAAAAAAAAAAAAAAAADJAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAEgAAAAAAAAAAAAAAAAAA +AAAAAABOAAAAEgAAAAAAAAAAAAAAAAAAAAAAAADdAAAAIAAAAAAAAAAAAAAAAAAAAAAAAABuAAAA +IgAAAAAAAAAAAAAAAAAAAAAAAABiAAAAEQAYACAQIAAAAAAACAAAAAAAAAAAbGliYy5zby42AF9f +cHJpbnRmX2NoawBleGl0AHNldGxvY2FsZQBwZXJyb3IAZGNnZXR0ZXh0AF9fc3RhY2tfY2hrX2Zh +aWwAcmVhZABfX2ZwcmludGZfY2hrAGxzZWVrAHN0ZGVycgBvcGVuAF9fY3hhX2ZpbmFsaXplAGJp +bmR0ZXh0ZG9tYWluAF9fbGliY19zdGFydF9tYWluAF9JVE1fZGVyZWdpc3RlclRNQ2xvbmVUYWJs +ZQBfX2dtb25fc3RhcnRfXwBfSnZfUmVnaXN0ZXJDbGFzc2VzAF9JVE1fcmVnaXN0ZXJUTUNsb25l +VGFibGUAR0xJQkNfMi4zLjQAR0xJQkNfMi40AEdMSUJDXzIuMi41AAAAAAAAAgACAAIAAwACAAIA +AgAAAAIABAACAAIAAAACAAQAAAACAAIAAAAAAAAAAQADAAEAAAAQAAAAAAAAAHQZaQkAAAQA9wAA +ABAAAAAUaWkNAAADAAMBAAAQAAAAdRppCQAAAgANAQAAAAAAAIANIAAAAAAACAAAAAAAAADwCgAA +AAAAAIgNIAAAAAAACAAAAAAAAACwCgAAAAAAAAgQIAAAAAAACAAAAAAAAAAIECAAAAAAAHAPIAAA +AAAABgAAAAEAAAAAAAAAAAAAAHgPIAAAAAAABgAAAAIAAAAAAAAAAAAAAIAPIAAAAAAABgAAAAMA +AAAAAAAAAAAAAIgPIAAAAAAABgAAAAQAAAAAAAAAAAAAAJAPIAAAAAAABgAAAAUAAAAAAAAAAAAA +AJgPIAAAAAAABgAAAAYAAAAAAAAAAAAAAKAPIAAAAAAABgAAAAcAAAAAAAAAAAAAAKgPIAAAAAAA +BgAAAAgAAAAAAAAAAAAAALAPIAAAAAAABgAAAAkAAAAAAAAAAAAAALgPIAAAAAAABgAAAAoAAAAA +AAAAAAAAAMAPIAAAAAAABgAAAAsAAAAAAAAAAAAAAMgPIAAAAAAABgAAAAwAAAAAAAAAAAAAANAP +IAAAAAAABgAAAA0AAAAAAAAAAAAAANgPIAAAAAAABgAAAA4AAAAAAAAAAAAAAOAPIAAAAAAABgAA +AA8AAAAAAAAAAAAAAOgPIAAAAAAABgAAABAAAAAAAAAAAAAAAPAPIAAAAAAABgAAABEAAAAAAAAA +AAAAAPgPIAAAAAAABgAAABIAAAAAAAAAAAAAACAQIAAAAAAABQAAABMAAAAAAAAAAAAAAEiD7AhI +iwVtByAASIXAdAL/0EiDxAjDAP81CgcgAP8lDAcgAA8fQAD/JRIHIABmkP8lEgcgAGaQ/yUSByAA +ZpD/JRIHIABmkP8lEgcgAGaQ/yUSByAAZpD/JSIHIABmkP8lIgcgAGaQ/yUiByAAZpD/JSIHIABm +kP8lKgcgAGaQ/yUqByAAZpD/JTIHIABmkAAAAAAAAAAAVVNIifVIjTUKAwAAifu/BgAAAEiD7Dhk +SIsEJSgAAABIiUQkKDHA6JT///9IjT2sAgAA6Fj///9IjTWmAgAASI09mQIAAOhN////g/sCdQZI +i30I6zb/y0iNPXUCAAB0K0iNNY8CAAAx/7oFAAAA6Cz///9Iiz3VBiAASInCvgEAAAAxwOhe//// +6ysx9jHA6Dv///+D+P+Jw3UlSI01dAIAADH/ugUAAADo8f7//0iJx+gh////vwEAAADoH////zHS +viiAAACJx+jh/v///8B0yUiNbCQHuiAAAACJ30iJ7ujR/v///8B0sUiNNS0CAAAxwEiJ6r8BAAAA +6Mf+//8xwEiLTCQoZEgzDCUoAAAAdAXokP7//0iDxDhbXcOQMe1JidFeSIniSIPk8FBUTI0FigEA +AEiNDRMBAABIjT28/v///xWOBSAA9A8fRAAASI096QUgAEiNBekFIABVSCn4SInlSIP4DnYVSIsF +LgUgAEiFwHQJXf/gZg8fRAAAXcMPH0AAZi4PH4QAAAAAAEiNPakFIABIjTWiBSAAVUgp/kiJ5UjB +/gNIifBIweg/SAHGSNH+dBhIiwVhBSAASIXAdAxd/+BmDx+EAAAAAABdww8fQABmLg8fhAAAAAAA +gD1xBSAAAHUnSIM9NwUgAABVSInldAxIiz06BSAA6O39///oSP///13GBUgFIAAB88MPH0AAZi4P +H4QAAAAAAEiNPZkCIABIgz8AdQvpXv///2YPH0QAAEiLBckEIABIhcB06VVIieX/0F3pQP///0FX +QVZBif9BVUFUTI0lTgIgAFVIjS1OAiAAU0mJ9kmJ1Uwp5UiD7AhIwf0D6Of8//9Ihe10IDHbDx+E +AAAAAABMiepMifZEif9B/xTcSIPDAUg53XXqSIPECFtdQVxBXUFeQV/DkGYuDx+EAAAAAADzwwAA +SIPsCEiDxAjDAAAAAQACAC9kZXYvY2Ryb20AZWplY3QAL3Vzci9zaGFyZS9sb2NhbGUAdXNhZ2U6 +IHZvbG5hbWUgWzxkZXZpY2UtZmlsZT5dCgB2b2xuYW1lACUzMi4zMnMKAAEbAzs8AAAABgAAAFj8 +//+IAAAAaPz//7AAAADY/P//yAAAAPj9//9YAAAAKP////gAAACY////QAEAAAAAAAAUAAAAAAAA +AAF6UgABeBABGwwHCJABBxAUAAAAHAAAAJj9//8rAAAAAAAAAAAAAAAUAAAAAAAAAAF6UgABeBAB +GwwHCJABAAAkAAAAHAAAAMj7//8QAAAAAA4QRg4YSg8LdwiAAD8aOyozJCIAAAAAFAAAAEQAAACw ++///aAAAAAAAAAAAAAAALAAAAFwAAAAI/P//HwEAAABBDhCGAkEOGIMDVQ5QAwUBDhhBDhBBDggA +AAAAAAAARAAAAIwAAAAo/v//ZQAAAABCDhCPAkIOGI4DRQ4gjQRCDiiMBUgOMIYGSA44gwdNDkBy +DjhBDjBBDihCDiBCDhhCDhBCDggAFAAAANQAAABQ/v//AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AoAAAAAAACwCgAAAAAAAAAAAAAA +AAAAAQAAAAAAAAABAAAAAAAAAAwAAAAAAAAAOAgAAAAAAAANAAAAAAAAAJQLAAAAAAAAGQAAAAAA +AACADSAAAAAAABsAAAAAAAAACAAAAAAAAAAaAAAAAAAAAIgNIAAAAAAAHAAAAAAAAAAIAAAAAAAA +APX+/28AAAAAmAIAAAAAAAAFAAAAAAAAAKAEAAAAAAAABgAAAAAAAADAAgAAAAAAAAoAAAAAAAAA +GQEAAAAAAAALAAAAAAAAABgAAAAAAAAAFQAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAWA8gAAAAAAAH +AAAAAAAAACgGAAAAAAAACAAAAAAAAAAQAgAAAAAAAAkAAAAAAAAAGAAAAAAAAAAeAAAAAAAAAAgA +AAAAAAAA+///bwAAAAABAAAIAAAAAP7//28AAAAA6AUAAAAAAAD///9vAAAAAAEAAAAAAAAA8P// +bwAAAAC6BQAAAAAAAPn//28AAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJgNIAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +CBAgAAAAAAA1ZjkzZjY2ZWNlMGI1ZDRkMDA2NjE1M2JlZDAxZjg3MDhhMzBlNi5kZWJ1ZwAAAAAF +PtqRAC5zaHN0cnRhYgAuaW50ZXJwAC5ub3RlLkFCSS10YWcALm5vdGUuZ251LmJ1aWxkLWlkAC5n +bnUuaGFzaAAuZHluc3ltAC5keW5zdHIALmdudS52ZXJzaW9uAC5nbnUudmVyc2lvbl9yAC5yZWxh +LmR5bgAuaW5pdAAucGx0AC5wbHQuZ290AC50ZXh0AC5maW5pAC5yb2RhdGEALmVoX2ZyYW1lX2hk +cgAuZWhfZnJhbWUALmluaXRfYXJyYXkALmZpbmlfYXJyYXkALmpjcgAuZHluYW1pYwAuZGF0YQAu +YnNzAC5nbnVfZGVidWdsaW5rAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAQAAAAIAAAAAAAAAOAIAAAAAAAA4AgAAAAAA +ABwAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAEwAAAAcAAAACAAAAAAAAAFQCAAAAAAAA +VAIAAAAAAAAgAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAACEAAAAHAAAAAgAAAAAAAAB0 +AgAAAAAAAHQCAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA0AAAA9v//bwIA +AAAAAAAAmAIAAAAAAACYAgAAAAAAACQAAAAAAAAABQAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAPgAA +AAsAAAACAAAAAAAAAMACAAAAAAAAwAIAAAAAAADgAQAAAAAAAAYAAAABAAAACAAAAAAAAAAYAAAA +AAAAAEYAAAADAAAAAgAAAAAAAACgBAAAAAAAAKAEAAAAAAAAGQEAAAAAAAAAAAAAAAAAAAEAAAAA +AAAAAAAAAAAAAABOAAAA////bwIAAAAAAAAAugUAAAAAAAC6BQAAAAAAACgAAAAAAAAABQAAAAAA +AAACAAAAAAAAAAIAAAAAAAAAWwAAAP7//28CAAAAAAAAAOgFAAAAAAAA6AUAAAAAAABAAAAAAAAA +AAYAAAABAAAACAAAAAAAAAAAAAAAAAAAAGoAAAAEAAAAAgAAAAAAAAAoBgAAAAAAACgGAAAAAAAA +EAIAAAAAAAAFAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAB0AAAAAQAAAAYAAAAAAAAAOAgAAAAAAAA4 +CAAAAAAAABcAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAegAAAAEAAAAGAAAAAAAAAFAI +AAAAAAAAUAgAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAH8AAAABAAAABgAA +AAAAAABgCAAAAAAAAGAIAAAAAAAAaAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAACIAAAA +AQAAAAYAAAAAAAAA0AgAAAAAAADQCAAAAAAAAMICAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAA +AAAAjgAAAAEAAAAGAAAAAAAAAJQLAAAAAAAAlAsAAAAAAAAJAAAAAAAAAAAAAAAAAAAABAAAAAAA +AAAAAAAAAAAAAJQAAAABAAAAAgAAAAAAAACgCwAAAAAAAKALAAAAAAAAWAAAAAAAAAAAAAAAAAAA +AAQAAAAAAAAAAAAAAAAAAACcAAAAAQAAAAIAAAAAAAAA+AsAAAAAAAD4CwAAAAAAADwAAAAAAAAA +AAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAqgAAAAEAAAACAAAAAAAAADgMAAAAAAAAOAwAAAAAAAAc +AQAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAALQAAAAOAAAAAwAAAAAAAACADSAAAAAAAIAN +AAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAADAAAAADwAAAAMAAAAAAAAAiA0g +AAAAAACIDQAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAzAAAAAEAAAADAAAA +AAAAAJANIAAAAAAAkA0AAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAANEAAAAG +AAAAAwAAAAAAAACYDSAAAAAAAJgNAAAAAAAAwAEAAAAAAAAGAAAAAAAAAAgAAAAAAAAAEAAAAAAA +AACDAAAAAQAAAAMAAAAAAAAAWA8gAAAAAABYDwAAAAAAAKgAAAAAAAAAAAAAAAAAAAAIAAAAAAAA +AAgAAAAAAAAA2gAAAAEAAAADAAAAAAAAAAAQIAAAAAAAABAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA +CAAAAAAAAAAAAAAAAAAAAOAAAAAIAAAAAwAAAAAAAAAgECAAAAAAABAQAAAAAAAAEAAAAAAAAAAA +AAAAAAAAACAAAAAAAAAAAAAAAAAAAADlAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAADQA +AAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAARBAA +AAAAAAD0AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==` + +var firstDeltas = sdtypes.StackDeltaArray{ + {Address: 0x850, Hints: sdtypes.UnwindHintKeep, + Info: deltaRSP(16, 0)}, + {Address: 0x856, Info: deltaRSP(24, 0)}, + {Address: 0x860, Hints: sdtypes.UnwindHintKeep, Info: deltaRSP(8, 0)}, + {Address: 0x8d1, Info: deltaRSP(16, 16)}, + {Address: 0x8d2, Info: deltaRSP(24, 16)}, + {Address: 0x8e7, Info: deltaRSP(80, 16)}, +} + +func TestExtractStackDeltasFromFilename(t *testing.T) { + buffer, err := base64.StdEncoding.DecodeString(usrBinVolname) + if err != nil { + t.Errorf("Failed to base64-decode the embedded executable?") + } + // 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") + } + 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) + } + defer os.Remove(exeFile.Name()) + filename := exeFile.Name() + + var data sdtypes.IntervalData + err = Extract(filename, &data) + if err != nil { + t.Errorf("%v", 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) + } +} diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/.gitignore b/libpf/nativeunwind/elfunwindinfo/testdata/.gitignore new file mode 100644 index 00000000..e015147b --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/testdata/.gitignore @@ -0,0 +1,4 @@ +helloworld +helloworld.pie +helloworld.stripped.pie +helloworld.arm64 diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/Makefile b/libpf/nativeunwind/elfunwindinfo/testdata/Makefile new file mode 100644 index 00000000..6fabd59f --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/testdata/Makefile @@ -0,0 +1,26 @@ +.PHONY: all + +BINARIES=helloworld \ + helloworld.pie \ + helloworld.stripped.pie \ + helloworld.arm64 + +# Use the default go executable if it is not specified otherwise. +GO_BINARY ?= go + +all: $(BINARIES) + +clean: + rm -f $(BINARIES) + +helloworld: + $(GO_BINARY) build -o $@ helloworld.go + +helloworld.pie: + $(GO_BINARY) build -buildmode=pie -o $@ helloworld.go + +helloworld.stripped.pie: + $(GO_BINARY) build -buildmode=pie -ldflags="-s -w" -o $@ helloworld.go + +helloworld.arm64: + GOARCH=arm64 $(GO_BINARY) build -o $@ helloworld.go diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/helloworld.go b/libpf/nativeunwind/elfunwindinfo/testdata/helloworld.go new file mode 100644 index 00000000..4832f8bf --- /dev/null +++ b/libpf/nativeunwind/elfunwindinfo/testdata/helloworld.go @@ -0,0 +1,17 @@ +/* + * 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 "fmt" + +func sayHi() { + fmt.Println("Hello World") +} + +func main() { + sayHi() +} diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/schrodinger-libpython3.8.so.1.0 b/libpf/nativeunwind/elfunwindinfo/testdata/schrodinger-libpython3.8.so.1.0 new file mode 100755 index 00000000..b87819ef Binary files /dev/null and b/libpf/nativeunwind/elfunwindinfo/testdata/schrodinger-libpython3.8.so.1.0 differ diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/test.so b/libpf/nativeunwind/elfunwindinfo/testdata/test.so new file mode 100755 index 00000000..c620211d Binary files /dev/null and b/libpf/nativeunwind/elfunwindinfo/testdata/test.so differ diff --git a/libpf/nativeunwind/intervalcache.go b/libpf/nativeunwind/intervalcache.go new file mode 100644 index 00000000..be198422 --- /dev/null +++ b/libpf/nativeunwind/intervalcache.go @@ -0,0 +1,33 @@ +/* + * 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 new file mode 100644 index 00000000..2e011558 --- /dev/null +++ b/libpf/nativeunwind/localintervalcache/localintervalcache.go @@ -0,0 +1,429 @@ +/* + * 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 new file mode 100644 index 00000000..4472b5e2 --- /dev/null +++ b/libpf/nativeunwind/localintervalcache/localintervalcache_test.go @@ -0,0 +1,587 @@ +/* + * 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 new file mode 100644 index 00000000..6c37c78e --- /dev/null +++ b/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider.go @@ -0,0 +1,77 @@ +/* + * 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 new file mode 100644 index 00000000..20c38d33 --- /dev/null +++ b/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider_test.go @@ -0,0 +1,431 @@ +/* + * 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/nativeunwind/stackdeltaprovider.go b/libpf/nativeunwind/stackdeltaprovider.go new file mode 100644 index 00000000..a507c44e --- /dev/null +++ b/libpf/nativeunwind/stackdeltaprovider.go @@ -0,0 +1,36 @@ +/* + * 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" + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" +) + +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 extracting stack deltas failed. + ExtractionErrors uint64 +} + +// StackDeltaProvider defines an interface for types that provide access to the stack deltas from +// executables. +type StackDeltaProvider interface { + // GetIntervalStructuresForFile inspects a single executable and extracts data that is needed + // to rebuild the stack for traces of this executable. + GetIntervalStructuresForFile(fileID host.FileID, elfRef *pfelf.Reference, + interval *sdtypes.IntervalData) error + + // GetAndResetStatistics returns the internal statistics for this provider and resets all + // values to 0. + GetAndResetStatistics() Statistics +} diff --git a/libpf/nativeunwind/stackdeltatypes/stackdeltatypes.go b/libpf/nativeunwind/stackdeltatypes/stackdeltatypes.go new file mode 100644 index 00000000..d73653fd --- /dev/null +++ b/libpf/nativeunwind/stackdeltatypes/stackdeltatypes.go @@ -0,0 +1,148 @@ +/* + * 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 stackdeltatypes provides types used to represent stack delta information as constructed +// by `nativeunwind.GetIntervalStructures` This information is a post-processed form of the +// stack delta information that is used in all relevant packages. +package stackdeltatypes + +// #include "../../../support/ebpf/stackdeltatypes.h" +import "C" + +const ( + // ABI is the current binary compatibility version. It is incremented + // if struct IntervalData, struct StackDelta or the meaning of their contents + // changes, and can be used to determine if the data is compatible + ABI = 15 + + // MinimumGap determines the minimum number of alignment bytes needed + // in order to keep the created STOP stack delta between functions + MinimumGap = 15 + + // UnwindOpcodes from the C header file + UnwindOpcodeCommand uint8 = C.UNWIND_OPCODE_COMMAND + UnwindOpcodeBaseCFA uint8 = C.UNWIND_OPCODE_BASE_CFA + UnwindOpcodeBaseSP uint8 = C.UNWIND_OPCODE_BASE_SP + UnwindOpcodeBaseFP uint8 = C.UNWIND_OPCODE_BASE_FP + UnwindOpcodeBaseLR uint8 = C.UNWIND_OPCODE_BASE_LR + UnwindOpcodeFlagDeref uint8 = C.UNWIND_OPCODEF_DEREF + + // UnwindCommands from the C header file + UnwindCommandInvalid int32 = C.UNWIND_COMMAND_INVALID + UnwindCommandStop int32 = C.UNWIND_COMMAND_STOP + UnwindCommandPLT int32 = C.UNWIND_COMMAND_PLT + UnwindCommandSignal int32 = C.UNWIND_COMMAND_SIGNAL + + // UnwindDeref handling from the C header file + UnwindDerefMask int32 = C.UNWIND_DEREF_MASK + UnwindDerefMultiplier int32 = C.UNWIND_DEREF_MULTIPLIER + + // UnwindHintNone indicates that no flags are set. + UnwindHintNone uint8 = 0 + // UnwindHintKeep flags important intervals that should not be removed + // (e.g. has CALL/SYSCALL assembly opcode, or is part of function prologue) + UnwindHintKeep uint8 = 1 + // UnwindHintGap indicates that the delta marks function end + UnwindHintGap uint8 = 4 +) + +// UnwindInfo contains the data needed to unwind PC, SP and FP +type UnwindInfo struct { + Opcode, FPOpcode, MergeOpcode uint8 + + Param, FPParam int32 +} + +// UnwindInfoInvalid is the stack delta info indicating invalid or unsupported PC. +var UnwindInfoInvalid = UnwindInfo{Opcode: UnwindOpcodeCommand, Param: UnwindCommandInvalid} + +// UnwindInfoStop is the stack delta info indicating root function of a stack. +var UnwindInfoStop = UnwindInfo{Opcode: UnwindOpcodeCommand, Param: UnwindCommandStop} + +// UnwindInfoSignal is the stack delta info indicating signal return frame. +var UnwindInfoSignal = UnwindInfo{Opcode: UnwindOpcodeCommand, Param: UnwindCommandSignal} + +// UnwindInfoFramePointer contains the description to unwind a frame with valid frame pointer. +var UnwindInfoFramePointer = UnwindInfo{ + Opcode: UnwindOpcodeBaseFP, + Param: 16, + FPOpcode: UnwindOpcodeBaseCFA, + FPParam: -16, +} + +// StackDelta defines the start address for the delta interval, along with +// the unwind information. +type StackDelta struct { + Address uint64 + Hints uint8 + Info UnwindInfo +} + +// StackDeltaArray defines an address space where consecutive entries establish +// intervals for the stack deltas +type StackDeltaArray []StackDelta + +// IntervalData contains everything that a userspace agent needs to have +// to populate eBPF maps for the kernel-space native unwinder to do its job: +type IntervalData struct { + // Deltas contains all stack deltas for a single binary. + // Two consecutive entries describe an interval. + Deltas StackDeltaArray +} + +// AddEx adds a new stack delta to the array. +func (deltas *StackDeltaArray) AddEx(delta StackDelta, sorted bool) { + num := len(*deltas) + if delta.Info.Opcode == UnwindOpcodeCommand { + // FP information is invalid/unused for command opcodes. + // But DWARF info often leaves bogus data there, so resetting it + // reduces the number of unique Info contents generated. + delta.Info.FPOpcode = UnwindOpcodeCommand + delta.Info.FPParam = UnwindCommandInvalid + } + if num > 0 && sorted { + prev := &(*deltas)[num-1] + if prev.Hints&UnwindHintGap != 0 && prev.Address+MinimumGap >= delta.Address { + // The previous opcode is end-of-function marker, and + // the gap is not large. Reduce deltas by overwriting it. + if num <= 1 || (*deltas)[num-2].Info != delta.Info { + *prev = delta + return + } + // The delta before end-of-function marker is same as + // what is being inserted now. Overwrite that. + prev = &(*deltas)[num-2] + *deltas = (*deltas)[:num-1] + } + if prev.Info == delta.Info { + prev.Hints |= delta.Hints & UnwindHintKeep + return + } + if prev.Address == delta.Address { + *prev = delta + return + } + } + *deltas = append(*deltas, delta) +} + +// Add adds a new stack delta from a sorted source. +func (deltas *StackDeltaArray) Add(delta StackDelta) { + deltas.AddEx(delta, true) +} + +// PackDerefParam compresses pre- and post-dereference parameters to single value +func PackDerefParam(preDeref, postDeref int32) (int32, bool) { + if postDeref < 0 || postDeref > 0x20 || postDeref%UnwindDerefMultiplier != 0 { + return 0, false + } + return preDeref + postDeref/UnwindDerefMultiplier, true +} + +// UnpackDerefParam splits the pre- and post-dereference parameters from single value +func UnpackDerefParam(param int32) (preDeref, postDeref int32) { + return param &^ UnwindDerefMask, (param & UnwindDerefMask) * UnwindDerefMultiplier +} diff --git a/libpf/nopanicslicereader/nopanicslicereader.go b/libpf/nopanicslicereader/nopanicslicereader.go new file mode 100644 index 00000000..d7113c4c --- /dev/null +++ b/libpf/nopanicslicereader/nopanicslicereader.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. + */ + +// nopanicslicereader provides little convenience utilities to read "native" endian +// values from a slice at given offset. Zeroes are returned on out of bounds access +// instead of panic. +package nopanicslicereader + +import ( + "encoding/binary" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +// Uint8 reads one 8-bit unsigned integer from given byte slice offset +func Uint8(b []byte, offs uint) uint8 { + if offs+1 > uint(len(b)) { + return 0 + } + return b[offs] +} + +// Uint16 reads one 16-bit unsigned integer from given byte slice offset +func Uint16(b []byte, offs uint) uint16 { + if offs+2 > uint(len(b)) { + return 0 + } + return binary.LittleEndian.Uint16(b[offs:]) +} + +// Uint32 reads one 32-bit unsigned integer from given byte slice offset +func Uint32(b []byte, offs uint) uint32 { + if offs+4 > uint(len(b)) { + return 0 + } + return binary.LittleEndian.Uint32(b[offs:]) +} + +// Int32 reads one 32-bit signed integer from given byte slice offset +func Int32(b []byte, offs uint) int32 { + if offs+4 > uint(len(b)) { + return 0 + } + return int32(binary.LittleEndian.Uint32(b[offs:])) +} + +// Uint64 reads one 64-bit unsigned integer from given byte slice offset +func Uint64(b []byte, offs uint) uint64 { + if offs+8 > uint(len(b)) { + return 0 + } + return binary.LittleEndian.Uint64(b[offs:]) +} + +// Ptr reads one native sized pointer from given byte slice offset +func Ptr(b []byte, offs uint) libpf.Address { + return libpf.Address(Uint64(b, offs)) +} + +// PtrDiff32 reads one 32-bit unsigned integer from given byte slice offset +// and returns it as an address +func PtrDiff32(b []byte, offs uint) libpf.Address { + return libpf.Address(Uint32(b, offs)) +} diff --git a/libpf/nopanicslicereader/nopanicslicereader_test.go b/libpf/nopanicslicereader/nopanicslicereader_test.go new file mode 100644 index 00000000..e39bf78f --- /dev/null +++ b/libpf/nopanicslicereader/nopanicslicereader_test.go @@ -0,0 +1,34 @@ +/* + * 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.go b/libpf/periodiccaller/periodiccaller.go new file mode 100644 index 00000000..82c84023 --- /dev/null +++ b/libpf/periodiccaller/periodiccaller.go @@ -0,0 +1,81 @@ +/* + * 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" + "time" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +// Start starts a timer that calls every until the is canceled. +func Start(ctx context.Context, interval time.Duration, callback func()) func() { + ticker := time.NewTicker(interval) + go func() { + defer ticker.Stop() + + for { + select { + case <-ticker.C: + callback() + case <-ctx.Done(): + return + } + } + }() + + return ticker.Stop +} + +// StartWithManualTrigger starts a timer that calls every +// from channel until the is canceled. Additionally the 'trigger' +// channel can be used to trigger callback immediately. +func StartWithManualTrigger(ctx context.Context, interval time.Duration, trigger chan bool, + callback func(manualTrigger bool)) func() { + ticker := time.NewTicker(interval) + go func() { + defer ticker.Stop() + + for { + select { + case <-ticker.C: + callback(false) + case <-trigger: + callback(true) + case <-ctx.Done(): + return + } + } + }() + + return ticker.Stop +} + +// StartWithJitter starts a timer that calls every +// until the is canceled. , [0..1], is used to add +/- jitter +// to at every iteration of the timer. +func StartWithJitter(ctx context.Context, baseDuration time.Duration, jitter float64, + callback func()) func() { + ticker := time.NewTicker(libpf.AddJitter(baseDuration, jitter)) + go func() { + defer ticker.Stop() + + for { + select { + case <-ticker.C: + callback() + case <-ctx.Done(): + return + } + ticker.Reset(libpf.AddJitter(baseDuration, jitter)) + } + }() + + return ticker.Stop +} diff --git a/libpf/periodiccaller/periodiccaller_test.go b/libpf/periodiccaller/periodiccaller_test.go new file mode 100644 index 00000000..ea750b05 --- /dev/null +++ b/libpf/periodiccaller/periodiccaller_test.go @@ -0,0 +1,154 @@ +/* + * 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.go b/libpf/pfelf/addressmapper.go new file mode 100644 index 00000000..bc3fe72e --- /dev/null +++ b/libpf/pfelf/addressmapper.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 pfelf implements functions for processing of ELF files and extracting data from +// them. This file provides a cacheable file offset to virtual address mapping. +package pfelf + +import ( + "debug/elf" + "os" +) + +// addressMapperPHDR contains the Program Header fields we need to cache for mapping +// file offsets to virtual addresses. +type addressMapperPHDR struct { + offset uint64 + vaddr uint64 + filesz uint64 +} + +// AddressMapper contains minimal information about PHDRs needed for address mapping +type AddressMapper struct { + phdrs []addressMapperPHDR +} + +var pageSizeMinusOne = uint64(os.Getpagesize()) - 1 + +// FileOffsetToVirtualAddress attempts to convert an on-disk file offset to the +// ELF virtual address where it would be mapped by default. +func (am *AddressMapper) FileOffsetToVirtualAddress(fileOffset uint64) (uint64, bool) { + for _, p := range am.phdrs { + // nolint:lll + // fileOffset may not correspond to any file offset present in the ELF program headers. + // Indeed, mmap alignment constraints may have forced the ELF loader to start a segment + // mapping before the actual start of the ELF LOAD segment. Because of this, we must + // perform the alignment logic on the offsets from the ELF program headers before comparing + // them to the fileOffset. + // Both [1] the kernel and [2] glibc (during dynamic linking) use the system page size when + // performing the alignment. Here we must replicate the same logic, hoping that no ELF + // loaders would do things differently (one could use a greater multiple of the page size + // when the ELF allows it, for example when p_align > pageSize). + // [1]: https://elixir.bootlin.com/linux/v5.10/source/fs/binfmt_elf.c#L367 + // [2]: https://github.com/bminor/glibc/blob/99468ed45f5a58f584bab60364af937eb6f8afda/elf/dl-load.c#L1159 + alignedOffset := p.offset &^ pageSizeMinusOne + + // Check if the offset corresponds to the current segment + if fileOffset >= alignedOffset && fileOffset < p.offset+p.filesz { + // Return the page-aligned Vaddr + return p.vaddr - (p.offset - fileOffset), true + } + } + return 0, false +} + +// NewAddressMapper returns an address mapper for given ELF File +func (f *File) GetAddressMapper() AddressMapper { + phdrs := make([]addressMapperPHDR, 0, 1) + for _, p := range f.Progs { + if p.Type != elf.PT_LOAD || p.Flags&elf.PF_X == 0 { + continue + } + phdrs = append(phdrs, addressMapperPHDR{ + offset: p.ProgHeader.Off, + vaddr: p.ProgHeader.Vaddr, + filesz: p.ProgHeader.Filesz, + }) + } + return AddressMapper{phdrs: phdrs} +} diff --git a/libpf/pfelf/addressmapper_test.go b/libpf/pfelf/addressmapper_test.go new file mode 100644 index 00000000..1c5c5aa8 --- /dev/null +++ b/libpf/pfelf/addressmapper_test.go @@ -0,0 +1,34 @@ +/* + * 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 pfelf + +import ( + "os" + "testing" + + "github.com/elastic/otel-profiling-agent/testsupport" + "github.com/stretchr/testify/assert" +) + +func assertFileToVA(t *testing.T, mapper AddressMapper, fileAddress, virtualAddress uint64) { + mappedAddress, ok := mapper.FileOffsetToVirtualAddress(fileAddress) + assert.True(t, ok) + assert.Equal(t, virtualAddress, mappedAddress) +} + +func TestAddressMapper(t *testing.T) { + debugExePath, err := testsupport.WriteTestExecutable2() + assert.NoError(t, err) + defer os.Remove(debugExePath) + + ef, err := Open(debugExePath) + assert.NoError(t, err) + + mapper := ef.GetAddressMapper() + assertFileToVA(t, mapper, 0x1000, 0x401000) + assertFileToVA(t, mapper, 0x1010, 0x401010) +} diff --git a/libpf/pfelf/data b/libpf/pfelf/data new file mode 100644 index 00000000..cd729e36 Binary files /dev/null and b/libpf/pfelf/data differ diff --git a/libpf/pfelf/elfopener.go b/libpf/pfelf/elfopener.go new file mode 100644 index 00000000..01bd0ab9 --- /dev/null +++ b/libpf/pfelf/elfopener.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 pfelf implements functions for processing of ELF files and extracting data from +// them. This file implements an interface to open ELF files from arbitrary location with name. + +package pfelf + +// ELFOpener is the interface to open ELF files from arbitrary location with given filename. +// +// Implementations must be safe to be called from different threads simultaneously. +type ELFOpener interface { + OpenELF(string) (*File, error) +} + +// SystemOpener implements ELFOpener by opening files from file system +type systemOpener struct{} + +func (systemOpener) OpenELF(file string) (*File, error) { + return Open(file) +} + +var SystemOpener systemOpener diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go new file mode 100644 index 00000000..11c936fc --- /dev/null +++ b/libpf/pfelf/file.go @@ -0,0 +1,867 @@ +/* + * 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 pfelf implements functions for processing of ELF files and extracting data from +// them. This file implements an independent ELF parser from debug.elf with different usage: +// - optimized for speed (and supports only ELF files for current CPU architecture) +// - loads only portions of the ELF really needed and accessed (minimizing CPU/RSS) +// - can handle partial ELF files without sections present +// - implements fast symbol lookup using gnu/sysv hashes +// - coredump notes parsing + +// The Executable and Linking Format (ELF) specification is available at: +// https://refspecs.linuxfoundation.org/elf/elf.pdf +// +// Other extensions we support are not well documented, but the following blog posts +// contain useful information about them: +// - DT_GNU_HASH symbol index: https://flapenguin.me/elf-dt-gnu-hash +// - NT_FILE coredump mappings: https://www.gabriel.urdhr.fr/2015/05/29/core-file/ + +package pfelf + +import ( + "bytes" + "debug/elf" + "errors" + "fmt" + "hash/crc32" + "io" + "os" + "path/filepath" + "sort" + "syscall" + "unsafe" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/readatbuf" + "github.com/elastic/otel-profiling-agent/libpf/remotememory" +) + +const ( + // maxBytesSmallSection is the maximum section size for small libpf + // parsed sections (e.g. notes and debug link) + maxBytesSmallSection = 4 * 1024 + + // maxBytesLargeSection is the maximum section size for large libpf + // parsed sections (e.g. symbol tables and string tables; libxul + // has about 4MB .dynstr) + maxBytesLargeSection = 16 * 1024 * 1024 +) + +// ErrSymbolNotFound is returned when requested symbol was not found +var ErrSymbolNotFound = errors.New("symbol not found") + +// ErrNotELF is returned when the file is not an ELF +var ErrNotELF = errors.New("not an ELF file") + +// File represents an open ELF file +type File struct { + // closer is called internally when resources for this File are to be released + closer io.Closer + + // elfReader is the ReadAt implementation used for this File + elfReader io.ReaderAt + + // ehFrame is a pointer to the PT_GNU_EH_FRAME segment of the ELF + ehFrame *Prog + + // ROData is a slice of pointers to the read-only data segments of the ELF + // These are sorted so that segments marked as "read" appear before those + // marked as "read-execute" + ROData []*Prog + + // Progs contains the program header + Progs []Prog + + // Sections contains the program sections if loaded + Sections []Section + + // neededIndexes contains the string tab indexes for DT_NEEDED tags + neededIndexes []int64 + + // neededIndexes contains the string tab index for DT_SONAME tag (or 0) + soNameIndex int64 + + // elfHeader is the ELF file header + elfHeader elf.Header64 + + // gnuHash contains the DT_GNU_HASH header address and data + gnuHash struct { + addr int64 + header gnuHashHeader + } + + // sysvHash contains the DT_HASH (SYS-V hash) header address and data + sysvHash struct { + addr int64 + header sysvHashHeader + } + + // stringsAddr is the virtual address for string table from the Dynamic section + stringsAddr int64 + + // symbolAddr is the virtual address for symbol table from the Dynamic section + symbolsAddr int64 + + // bias is the load bias for ELF files inside core dump + bias libpf.Address + + // InsideCore indicates that this ELF is mapped from a coredump ELF + InsideCore bool + + // Fields to mimic elf.debug + Type elf.Type + Machine elf.Machine + Entry uint64 +} + +var _ libpf.SymbolFinder = &File{} + +// sysvHashHeader is the ELF DT_HASH section header +type sysvHashHeader struct { + numBuckets uint32 + numSymbols uint32 +} + +// gnuHashHeader is the ELF DT_GNU_HASH section header +type gnuHashHeader struct { + numBuckets uint32 + symbolOffset uint32 + bloomSize uint32 + bloomShift uint32 +} + +// Prog represents a program header, and data associated with it +type Prog struct { + elf.ProgHeader + + // elfReader is the same ReadAt as used for the File + elfReader io.ReaderAt +} + +// Section represents a section header, and data associated with it +type Section struct { + elf.SectionHeader + + // Embed ReaderAt for ReadAt method. + io.ReaderAt + + // Do not embed SectionReader directly, or as public member. We can't + // return the same copy to multiple callers, otherwise they corrupt + // each other's reader file position. + sr *io.SectionReader +} + +// Open opens the named file using os.Open and prepares it for use as an ELF binary. +func Open(name string) (*File, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + + // Wrap it in a cacher as we often do short reads + buffered, err := readatbuf.New(f, 1024, 4) + if err != nil { + return nil, err + } + + ff, err := newFile(buffered, f, 0, false) + if err != nil { + f.Close() + return nil, err + } + return ff, nil +} + +// Close closes the File. +func (f *File) Close() (err error) { + if f.closer != nil { + err = f.closer.Close() + f.closer = nil + } + return +} + +// NewFile creates a new ELF file object that borrows the given reader. +func NewFile(r io.ReaderAt, loadAddress uint64, hasMusl bool) (*File, error) { + return newFile(r, nil, loadAddress, hasMusl) +} + +func newFile(r io.ReaderAt, closer io.Closer, loadAddress uint64, hasMusl bool) (*File, error) { + f := &File{ + elfReader: r, + InsideCore: loadAddress != 0, + closer: closer, + } + + hdr := &f.elfHeader + if _, err := r.ReadAt(libpf.SliceFrom(hdr), 0); err != nil { + return nil, err + } + if !bytes.Equal(hdr.Ident[0:4], []byte{0x7f, 'E', 'L', 'F'}) { + return nil, ErrNotELF + } + if elf.Class(hdr.Ident[elf.EI_CLASS]) != elf.ELFCLASS64 || + elf.Data(hdr.Ident[elf.EI_DATA]) != elf.ELFDATA2LSB || + elf.Version(hdr.Ident[elf.EI_VERSION]) != elf.EV_CURRENT { + return nil, fmt.Errorf("unsupported ELF file: %v", hdr.Ident) + } + + // fill the Machine and Type fields + f.Machine = elf.Machine(hdr.Machine) + f.Type = elf.Type(hdr.Type) + f.Entry = hdr.Entry + + // if number of program headers is 0 this is likely not the ELF file we + // are interested in + if hdr.Phnum == 0 { + return nil, fmt.Errorf("ELF with zero Program headers (type: %v)", hdr.Type) + } + + progs := make([]elf.Prog64, hdr.Phnum) + if _, err := r.ReadAt(libpf.SliceFrom(progs), int64(hdr.Phoff)); err != nil { + return nil, err + } + + f.Progs = make([]Prog, hdr.Phnum) + virtualBase := ^uint64(0) + for i, ph := range progs { + p := &f.Progs[i] + p.ProgHeader = elf.ProgHeader{ + Type: elf.ProgType(ph.Type), + Flags: elf.ProgFlag(ph.Flags), + Off: ph.Off, + Vaddr: ph.Vaddr, + Paddr: ph.Paddr, + Filesz: ph.Filesz, + Memsz: ph.Memsz, + Align: ph.Align, + } + p.elfReader = r + + if p.Type == elf.PT_LOAD { + if p.Vaddr < virtualBase { + virtualBase = p.Vaddr + } + andFlags := p.Flags & (elf.PF_R | elf.PF_W | elf.PF_X) + if andFlags == elf.PF_R || andFlags == (elf.PF_R|elf.PF_X) { + f.ROData = append(f.ROData, p) + } + } + } + if loadAddress != 0 { + // Calculate the bias for coredump files + f.bias = libpf.Address(loadAddress - virtualBase) + } + + // We sort the ROData so that we preferentially access those that are marked + // as "read" before we access those that are written as "read-execute" + sort.Slice(f.ROData, func(i, j int) bool { + // The &'s here are just in case one segment has PF_MASK_PROC set + return f.ROData[i].Flags&(elf.PF_R|elf.PF_X) < + f.ROData[j].Flags&(elf.PF_R|elf.PF_X) + }) + + for i := range f.Progs { + p := &f.Progs[i] + if p.Filesz <= 0 { + continue + } + switch p.ProgHeader.Type { + case elf.PT_DYNAMIC: + rdr, err := p.DataReader(maxBytesLargeSection) + if err != nil { + continue + } + var dyn elf.Dyn64 + var bias int64 + if !hasMusl { + // glibc adjusts the PT_DYNAMIC table to contain + // the mapped virtual addresses. Convert them back + // to file virtual addresses. + bias = int64(f.bias) + } + for { + if _, err := rdr.Read(libpf.SliceFrom(&dyn)); err != nil { + break + } + adjustedVal := int64(dyn.Val) + if adjustedVal >= bias { + adjustedVal -= bias + } + switch elf.DynTag(dyn.Tag) { + case elf.DT_NEEDED: + f.neededIndexes = append(f.neededIndexes, int64(dyn.Val)) + case elf.DT_SONAME: + f.soNameIndex = int64(dyn.Val) + case elf.DT_HASH: + f.sysvHash.addr = adjustedVal + case elf.DT_STRTAB: + f.stringsAddr = adjustedVal + case elf.DT_SYMTAB: + f.symbolsAddr = adjustedVal + case elf.DT_GNU_HASH: + f.gnuHash.addr = adjustedVal + } + } + case elf.PT_GNU_EH_FRAME: + f.ehFrame = p + } + } + + return f, nil +} + +// getString extracts a null terminated string from an ELF string table +func getString(section []byte, start int) (string, bool) { + if start < 0 || start >= len(section) { + return "", false + } + slen := bytes.IndexByte(section[start:], 0) + if slen < 0 { + return "", false + } + return string(section[start : start+slen]), true +} + +// LoadSections loads the ELF file sections +func (f *File) LoadSections() error { + if f.InsideCore { + // Do not look at section headers from ELF inside a coredump. Most + // notably musl c-library can reuse the section headers area for + // memory allocator, and this would return garbage. + return errors.New("section headers are not available for ELF inside coredump") + } + if f.Sections != nil { + // Already loaded. + return nil + } + + hdr := &f.elfHeader + if hdr.Shnum == 0 { + // No sections. Nothing to do. + return nil + } + if hdr.Shnum > 0 && hdr.Shstrndx >= hdr.Shnum { + return fmt.Errorf("invalid ELF section string table index (%d / %d)", + hdr.Shstrndx, hdr.Shnum) + } + + // Load section headers + sections := make([]elf.Section64, hdr.Shnum) + if _, err := f.elfReader.ReadAt(libpf.SliceFrom(sections), int64(hdr.Shoff)); err != nil { + return err + } + + f.Sections = make([]Section, hdr.Shnum) + for i, sh := range sections { + s := &f.Sections[i] + s.SectionHeader = elf.SectionHeader{ + Type: elf.SectionType(sh.Type), + Flags: elf.SectionFlag(sh.Flags), + Addr: sh.Addr, + Offset: sh.Off, + Size: sh.Size, + Link: sh.Link, + Info: sh.Info, + Addralign: sh.Addralign, + Entsize: sh.Entsize, + FileSize: sh.Size, + } + s.sr = io.NewSectionReader(f.elfReader, int64(s.Offset), int64(s.FileSize)) + s.ReaderAt = s.sr + } + + // Load the section name string table + strsh := f.Sections[hdr.Shstrndx] + if strsh.FileSize >= 1024*1024 { + return fmt.Errorf("section headers string table too large (%d)", + strsh.FileSize) + } + strtab, err := strsh.Data(maxBytesLargeSection) + if err != nil { + return err + } + for i := range f.Sections { + sh := &f.Sections[i] + var ok bool + sh.Name, ok = getString(strtab, int(sections[i].Name)) + if !ok { + return fmt.Errorf("bad section name index (section %d, index %d/%d)", + i, sections[i].Name, len(strtab)) + } + } + + return nil +} + +// Section returns a section with the given name, or nil if no such section exists. +func (f *File) Section(name string) *Section { + if f.InsideCore { + return nil + } + if err := f.LoadSections(); err != nil { + f.InsideCore = true + return nil + } + for i := range f.Sections { + s := &f.Sections[i] + if s.Name == name { + return s + } + } + return nil +} + +// ReadVirtualMemory reads bytes from given virtual address +func (f *File) ReadVirtualMemory(p []byte, addr int64) (int, error) { + if len(p) == 0 { + return 0, nil + } + for _, ph := range f.Progs { + // Search for the Program header that contains the start address. + // ReadVirtualMemory() supports ReadAt() style indication of reading + // less bytes then requested, so addr+len(p) can be an address beyond + // the segment and ReadAt() will give short read. + if ph.Type == elf.PT_LOAD && uint64(addr) >= ph.Vaddr && + uint64(addr) < ph.Vaddr+ph.Memsz { + return ph.ReadAt(p, addr-int64(ph.Vaddr)) + } + } + return 0, fmt.Errorf("no matching segment for 0x%x", uint64(addr)) +} + +// EHFrame constructs a Program header with the EH Frame sections +func (f *File) EHFrame() (*Prog, error) { + if f.ehFrame == nil { + return nil, errors.New("no PT_GNU_EH_FRAME tag found") + } + // Find matching PT_LOAD segment + p := f.ehFrame + for i := range f.Progs { + ph := &f.Progs[i] + if ph.Type != elf.PT_LOAD || p.Vaddr < ph.Vaddr || + p.Vaddr >= ph.Vaddr+ph.Filesz { + continue + } + // Normally the LOAD segment contains .rodata, .eh_frame_hdr + // and .eh_frame. Craft a subset segment that contains the data + // from start of the PT_GNU_EH_FRAME start until end of the LOAD + // segment. + offs := p.Vaddr - ph.Vaddr + return &Prog{ + ProgHeader: elf.ProgHeader{ + Type: ph.Type, + Flags: ph.Flags, + Off: ph.Off + offs, + Vaddr: ph.Vaddr + offs, + Paddr: ph.Paddr + offs, + Filesz: ph.Filesz - offs, + Memsz: ph.Memsz - offs, + Align: ph.Align, + }, + elfReader: f.elfReader, + }, nil + } + return nil, errors.New("no PT_LOAD segment for PT_GNU_EH_FRAME found") +} + +// GetBuildID returns the ELF BuildID if present +func (f *File) GetBuildID() (string, error) { + s := f.Section(".note.gnu.build-id") + if s == nil { + s = f.Section(".notes") + } + if s == nil { + return "", ErrNoBuildID + } + data, err := s.Data(maxBytesSmallSection) + if err != nil { + return "", err + } + + return getBuildIDFromNotes(data) +} + +// 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) { + note := f.Section(".gnu_debuglink") + if note == nil { + return "", 0, ErrNoDebugLink + } + + d, err := note.Data(maxBytesSmallSection) + if err != nil { + return "", 0, fmt.Errorf("could not read link: %w", ErrNoDebugLink) + } + return ParseDebugLink(d) +} + +// OpenDebugLink tries to locate and open the corresponding debug ELF for this DSO. +func (f *File) OpenDebugLink(elfFilePath string, elfOpener ELFOpener) ( + debugELF *File, debugFile string) { + // Get the debug link + linkName, linkCRC32, err := f.GetDebugLink() + if err != nil { + // Treat missing or corrupt tag as soft error. + return + } + + // Try to find the debug file + executablePath := filepath.Dir(elfFilePath) + for _, debugPath := range []string{"/usr/lib/debug/"} { + debugFile = filepath.Join(debugPath, executablePath, linkName) + debugELF, err = elfOpener.OpenELF(debugFile) + if err != nil { + continue + } + if debugELF.Section(".debug_frame") == nil { + debugELF.Close() + continue + } + fileCRC32, err := debugELF.CRC32() + if err != nil || fileCRC32 != linkCRC32 { + debugELF.Close() + continue + } + return debugELF, debugFile + } + return +} + +// CRC32 calculates the .gnu_debuglink compatible CRC-32 of the ELF file +func (f *File) CRC32() (int32, error) { + h := crc32.NewIEEE() + sr := io.NewSectionReader(f.elfReader, 0, 1<<63-1) + if _, err := io.Copy(h, sr); err != nil { + return 0, fmt.Errorf("unable to compute CRC32: %v (failed copy)", err) + } + return int32(h.Sum32()), nil +} + +// ReadAt implements the io.ReaderAt interface +func (ph *Prog) ReadAt(p []byte, off int64) (n int, err error) { + // First load as much as possible from the disk + if uint64(off) < ph.Filesz { + max := len(p) + if int64(max) > int64(ph.Filesz)-off { + max = int(int64(ph.Filesz) - off) + } + + n, err = ph.elfReader.ReadAt(p[0:max], int64(ph.Off)+off) + if n == 0 && errors.Is(err, syscall.EFAULT) { + // Read zeroes from sparse file holes + for i := range p[0:max] { + p[i] = 0 + } + n = max + } + if n != max || err != nil { + return n, err + } + off += int64(n) + } + + // The gap between Filesz and Memsz is allocated by dynamic loader as + // anonymous pages, and zero initialized. Read zeroes from this area. + if n < len(p) && uint64(off) < ph.Memsz { + max := len(p) - n + if int64(max) > int64(ph.Memsz)-off { + max = int(int64(ph.Memsz) - off) + } + for i := range p[n : n+max] { + p[i] = 0 + } + n += max + } + + if n != len(p) { + return n, io.EOF + } + return n, nil +} + +// Open returns a new ReadSeeker reading the ELF program body. +func (ph *Prog) Open() io.ReadSeeker { + return io.NewSectionReader(ph, 0, 1<<63-1) +} + +// Data loads the whole program header referenced data, and returns it as slice. +func (ph *Prog) Data(maxSize uint) ([]byte, error) { + if ph.Filesz > uint64(maxSize) { + return nil, fmt.Errorf("segment size %d is too large", ph.Filesz) + } + p := make([]byte, ph.Filesz) + _, err := ph.ReadAt(p, 0) + return p, err +} + +// DataReader loads the whole program header referenced data, and returns reader to it. +func (ph *Prog) DataReader(maxSize uint) (io.Reader, error) { + p, err := ph.Data(maxSize) + if err != nil { + return nil, err + } + return bytes.NewReader(p), nil +} + +// 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") + } + if sh.FileSize > uint64(maxSize) { + return nil, fmt.Errorf("section size %d is too large", sh.FileSize) + } + p := make([]byte, sh.FileSize) + _, err := sh.ReadAt(p, 0) + return p, err +} + +// ReadAt reads bytes from given virtual address +func (f *File) ReadAt(p []byte, addr int64) (int, error) { + return f.ReadVirtualMemory(p, addr) +} + +// GetRemoteMemory returns RemoteMemory interface for the core dump +func (f *File) GetRemoteMemory() remotememory.RemoteMemory { + return remotememory.RemoteMemory{ + ReaderAt: f, + Bias: f.bias, + } +} + +// readAndMatchSymbol reads symbol table data expecting given symbol +func (f *File) readAndMatchSymbol(n uint32, name libpf.SymbolName) (libpf.Symbol, bool) { + var sym elf.Sym64 + + // Read symbol descriptor and expected name + symSz := int64(unsafe.Sizeof(sym)) + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(&sym), + f.symbolsAddr+int64(n)*symSz); err != nil { + return libpf.Symbol{}, false + } + slen := len(name) + 1 + sname := make([]byte, slen) + if _, err := f.ReadVirtualMemory(sname, f.stringsAddr+int64(sym.Name)); err != nil { + return libpf.Symbol{}, false + } + + // Verify that name matches + if sname[slen-1] != 0 || libpf.SymbolName(sname[:slen-1]) != name { + return libpf.Symbol{}, false + } + + return libpf.Symbol{ + Name: name, + Address: libpf.SymbolValue(sym.Value), + Size: int(sym.Size), + }, true +} + +// calcGNUHash calculates a GNU symbol hash +func calcGNUHash(s libpf.SymbolName) uint32 { + h := uint32(5381) + for _, c := range []byte(s) { + h += h*32 + uint32(c) + } + return h +} + +// calcSysvHash calculates a sysv symbol hash +func calcSysvHash(s libpf.SymbolName) uint32 { + h := uint32(0) + for _, c := range []byte(s) { + h = 16*h + uint32(c) + h ^= h >> 24 & 0xf0 + } + return h & 0xfffffff +} + +// LookupSymbol searches for a given symbol in the ELF +func (f *File) LookupSymbol(symbol libpf.SymbolName) (*libpf.Symbol, error) { + if f.gnuHash.addr != 0 { + // 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 + if hdr.numBuckets == 0 { + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(hdr), f.gnuHash.addr); err != nil { + return nil, err + } + if hdr.numBuckets == 0 || hdr.bloomSize == 0 { + return nil, errors.New("DT_GNU_HASH corrupt") + } + } + ptrSize := int64(unsafe.Sizeof(uint(0))) + ptrSizeBits := uint32(8 * ptrSize) + + // First check the Bloom filter if the symbol exists in the hash table or not. + var bloom uint + h := calcGNUHash(symbol) + offs := f.gnuHash.addr + int64(unsafe.Sizeof(gnuHashHeader{})) + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(&bloom), offs+ + ptrSize*int64((h/ptrSizeBits)%hdr.bloomSize)); err != nil { + return nil, err + } + mask := uint(1)<<(h%ptrSizeBits) | + uint(1)<<((h>>hdr.bloomShift)%ptrSizeBits) + if bloom&mask != mask { + return nil, ErrSymbolNotFound + } + + // Read the initial symbol index to start looking from + offs += int64(hdr.bloomSize) * int64(unsafe.Sizeof(bloom)) + var i uint32 + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(&i), + offs+4*int64(h%hdr.numBuckets)); err != nil { + return nil, err + } + if i == 0 { + return nil, ErrSymbolNotFound + } + + // Search the hash bucket + offs += int64(4*hdr.numBuckets + 4*(i-hdr.symbolOffset)) + h |= 1 + for { + var h2 uint32 + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(&h2), offs); err != nil { + return nil, err + } + // Do a full match of the symbol if the symbol hash matches + if h == h2|1 { + if s, ok := f.readAndMatchSymbol(i, symbol); ok { + return &s, nil + } + } + // Was this last entry in the bucket? + if h2&1 != 0 { + break + } + offs += 4 + i++ + } + } else if f.sysvHash.addr != 0 { + // Normal ELF symbol lookup. Refer to ELF spec, part 2 "Hash Table" (2-19) + hdr := &f.sysvHash.header + if hdr.numBuckets == 0 { + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(hdr), f.sysvHash.addr); err != nil { + return nil, err + } + if hdr.numBuckets == 0 { + return nil, errors.New("DT_HASH corrupt") + } + } + var i uint32 + offs := f.sysvHash.addr + int64(unsafe.Sizeof(*hdr)) + h := calcSysvHash(symbol) + bucket := int64(h % hdr.numBuckets) + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(&i), offs+4*bucket); err != nil { + return nil, err + } + offs += 4 * int64(hdr.numBuckets) + for i != 0 && i < hdr.numSymbols { + if s, ok := f.readAndMatchSymbol(i, symbol); ok { + return &s, nil + } + if _, err := f.ReadVirtualMemory(libpf.SliceFrom(&i), offs+4*int64(i)); err != nil { + return nil, err + } + } + } else { + return nil, errors.New("symbol hash not present") + } + + return nil, ErrSymbolNotFound +} + +// LookupSymbol searches for a given symbol in the ELF +func (f *File) LookupSymbolAddress(symbol libpf.SymbolName) (libpf.SymbolValue, error) { + s, err := f.LookupSymbol(symbol) + if err != nil { + return libpf.SymbolValueInvalid, err + } + return s.Address, nil +} + +// loadSymbolTable reads given symbol table +func (f *File) loadSymbolTable(name string) (*libpf.SymbolMap, error) { + symTab := f.Section(name) + if symTab == nil { + return nil, fmt.Errorf("failed to read %v: section not present", name) + } + if symTab.Link >= uint32(len(f.Sections)) { + return nil, fmt.Errorf("failed to read %v strtab: link %v out of range", + name, symTab.Link) + } + strTab := f.Sections[symTab.Link] + strs, err := strTab.Data(maxBytesLargeSection) + if err != nil { + return nil, fmt.Errorf("failed to read %v: %v", strTab.Name, err) + } + syms, err := symTab.Data(maxBytesLargeSection) + if err != nil { + return nil, fmt.Errorf("failed to read %v: %v", name, err) + } + + symMap := libpf.SymbolMap{} + symSz := int(unsafe.Sizeof(elf.Sym64{})) + for i := 0; i < len(syms); i += symSz { + sym := (*elf.Sym64)(unsafe.Pointer(&syms[i])) + name, ok := getString(strs, int(sym.Name)) + if !ok { + continue + } + symMap.Add(libpf.Symbol{ + Name: libpf.SymbolName(name), + Address: libpf.SymbolValue(sym.Value), + Size: int(sym.Size), + }) + } + symMap.Finalize() + + return &symMap, nil +} + +// ReadSymbols reads the full dynamic symbol table from the ELF +func (f *File) ReadSymbols() (*libpf.SymbolMap, error) { + return f.loadSymbolTable(".symtab") +} + +// ReadDynamicSymbols reads the full dynamic symbol table from the ELF +func (f *File) ReadDynamicSymbols() (*libpf.SymbolMap, error) { + return f.loadSymbolTable(".dynsym") +} + +// DynString returns the strings listed for the given tag in the file's dynamic +// program header. +func (f *File) DynString(tag elf.DynTag) ([]string, error) { + var indexes []int64 + switch tag { + case elf.DT_NEEDED: + indexes = f.neededIndexes + case elf.DT_SONAME: + indexes = []int64{f.soNameIndex} + case elf.DT_RPATH, elf.DT_RUNPATH: + return nil, fmt.Errorf("unsupported tag %v", tag) + default: + return nil, fmt.Errorf("non-string-valued tag %v", tag) + } + + rm := f.GetRemoteMemory() + dynStrings := make([]string, 0, len(indexes)) + for _, ndx := range indexes { + strAddr := libpf.Address(f.stringsAddr + ndx) + dynStrings = append(dynStrings, rm.String(strAddr)) + } + return dynStrings, nil +} + +// IsGolang determines if this ELF is a Golang executable +func (f *File) IsGolang() bool { + return f.Section(".go.buildinfo") != nil || f.Section(".gopclntab") != nil +} diff --git a/libpf/pfelf/file_test.go b/libpf/pfelf/file_test.go new file mode 100644 index 00000000..1f9d1a9a --- /dev/null +++ b/libpf/pfelf/file_test.go @@ -0,0 +1,87 @@ +/* + * 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 pfelf + +import ( + "os" + "testing" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/testsupport" + "github.com/stretchr/testify/assert" +) + +func getPFELF(path string, t *testing.T) *File { + file, err := Open(path) + assert.Nil(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)) +} + +func lookupSymbolAddress(ef *File, name libpf.SymbolName) libpf.SymbolValue { + val, _ := ef.LookupSymbolAddress(name) + return val +} + +func TestPFELFSymbols(t *testing.T) { + exePath, err := testsupport.WriteSharedLibrary() + if err != nil { + t.Fatalf("Failed to write test executable: %v", err) + } + defer os.Remove(exePath) + + ef, err := Open(exePath) + if err != nil { + t.Fatalf("Failed to open test executable: %v", 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) + + // 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) +} + +func TestPFELFSections(t *testing.T) { + elfFile, err := Open("testdata/fixed-address") + if !assert.Nil(t, err) { + return + } + 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)) + + // Try to find a section that does not exist + sh = elfFile.Section(".tea_section") + assert.Nil(t, sh) + } +} + +func testPFELFIsGolang(t *testing.T, filename string, isGoExpected bool) { + ef := getPFELF(filename, t) + defer ef.Close() + assert.Equal(t, ef.IsGolang(), isGoExpected) +} + +func TestPFELFIsGolang(t *testing.T) { + testPFELFIsGolang(t, "testdata/go-binary", true) + testPFELFIsGolang(t, "testdata/without-debug-syms", false) +} diff --git a/libpf/pfelf/pfelf.go b/libpf/pfelf/pfelf.go new file mode 100644 index 00000000..f73a6847 --- /dev/null +++ b/libpf/pfelf/pfelf.go @@ -0,0 +1,505 @@ +/* + * 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 pfelf implements functions for processing of ELF files and extracting data from +// them. This file provides convenience functions for golang debug/elf standard library. +package pfelf + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "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) + } + + _, 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 +} + +// HasDWARFData returns true if the provided ELF file contains actionable DWARF debugging +// information. +// This function does not call `elfFile.DWARF()` on purpose, as it can be extremely expensive in +// terms of CPU/memory, possibly uncompressing all data in `.zdebug_` sections. +// This function being used extensively by the indexing service, it is preferable to keep it +// lightweight. +func HasDWARFData(elfFile *elf.File) bool { + hasBuildID := false + hasDebugStr := false + for _, section := range elfFile.Sections { + // NOBITS indicates that the section is actually empty, regardless of the size in the + // section header. + if section.Type == elf.SHT_NOBITS { + continue + } + + if section.Name == ".note.gnu.build-id" { + hasBuildID = true + } + + if section.Name == ".debug_str" || section.Name == ".zdebug_str" || + section.Name == ".debug_str.dwo" { + hasDebugStr = section.Size > 0 + } + + // Some files have suspicious near-empty, partially stripped sections; consider them as not + // having DWARF data. + // The simplest binary gcc 10 can generate ("return 0") has >= 48 bytes for each section. + // Let's not worry about executables that may not verify this, as they would not be of + // interest to us. + if section.Size < 32 { + continue + } + + if section.Name == ".debug_info" || section.Name == ".zdebug_info" { + return true + } + } + + // Some alternate debug files only have a .debug_str section. For these we want to return true. + // Use the absence of program headers and presence of a Build ID as heuristic to identify + // alternate debug files. + return len(elfFile.Progs) == 0 && hasBuildID && hasDebugStr +} + +var ErrNoDebugLink = errors.New("no debug link") + +// ParseDebugLink parses the name and CRC32 of the debug info file from the provided section data. +// Error is returned if the data is malformed. +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") + } + linkName = strings.ToValidUTF8(string(data[:strEnd]), "") + + strEnd++ + // The link contains 0 to 3 bytes of padding after the null character, CRC32 is 32-bit aligned + crc32StartIdx := strEnd + ((4 - (strEnd & 3)) & 3) + if crc32StartIdx+4 > len(data) { + return "", 0, fmt.Errorf("malformed debug link, no CRC32 (len %v, start index %v)", + len(data), crc32StartIdx) + } + + linkCRC32 := binary.LittleEndian.Uint32(data[crc32StartIdx : crc32StartIdx+4]) + + return linkName, int32(linkCRC32), nil +} + +func getSectionData(elfFile *elf.File, sectionName string) ([]byte, error) { + section := elfFile.Section(sectionName) + if section == nil { + return nil, fmt.Errorf("failed to open the %s section", sectionName) + } + data, err := section.Data() + if err != nil { + return nil, fmt.Errorf("failed to read data from section %s: %v", sectionName, err) + } + return data, nil +} + +// GetDebugLink reads and parses the .gnu_debuglink section of given ELF file. +// Error is returned if the data is malformed. If the link does not exist then +// ErrNoDebugLink is returned. +func GetDebugLink(elfFile *elf.File) (linkName string, crc32 int32, err error) { + // The .gnu_debuglink section is not always present + sectionData, err := getSectionData(elfFile, ".gnu_debuglink") + if err != nil { + return "", 0, ErrNoDebugLink + } + + return ParseDebugLink(sectionData) +} + +var ErrNoBuildID = errors.New("no build ID") +var ubuntuKernelSignature = regexp.MustCompile(` \(Ubuntu[^)]*\)\n$`) + +// GetKernelVersionBytes returns the kernel version from a kernel image, as it appears in +// /proc/version +// +// This makes the assumption that the string is the first one in ".rodata" that starts with +// "Linux version ". +func GetKernelVersionBytes(elfFile *elf.File) ([]byte, error) { + sectionData, err := getSectionData(elfFile, ".rodata") + if err != nil { + return nil, fmt.Errorf("failed to read kernel version: %v", err) + } + + // Prepend a null character to make sure this is the beginning of a string + procVersionContents := append([]byte{0x0}, []byte("Linux version ")...) + + startIdx := bytes.Index(sectionData, procVersionContents) + if startIdx < 0 { + return nil, fmt.Errorf("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)") + } + + versionBytes := sectionData[startIdx : startIdx+endIdx] + + // Ubuntu has some magic sauce that adds an extra signature at the end of the linux_banner + // string in init/version.c which is being extracted here. We replace it with the empty string + // to ensure it matches the contents of /proc/version, as extracted by the host agent. + return ubuntuKernelSignature.ReplaceAllLiteral(versionBytes, []byte{'\n'}), nil +} + +// GetBuildID extracts the build ID from the provided ELF file. This is read from +// the .note.gnu.build-id or .notes section of the ELF, and may not exist. If no build ID is present +// an ErrNoBuildID is returned. +func GetBuildID(elfFile *elf.File) (string, error) { + sectionData, err := getSectionData(elfFile, ".note.gnu.build-id") + if err != nil { + sectionData, err = getSectionData(elfFile, ".notes") + if err != nil { + return "", ErrNoBuildID + } + } + + return getBuildIDFromNotes(sectionData) +} + +// GetBuildIDFromNotesFile returns the build ID contained in a file with the format of an ELF notes +// section. +func GetBuildIDFromNotesFile(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("could not open %s: %w", filePath, err) + } + defer file.Close() + data, err := io.ReadAll(file) + if err != nil { + return "", fmt.Errorf("could not read %s: %w", filePath, err) + } + return getBuildIDFromNotes(data) +} + +// getBuildIDFromNotes returns the build ID from an ELF notes section data. +func getBuildIDFromNotes(notes []byte) (string, error) { + // 0x3 is the "Build ID" type. Not sure where this is standardized. + buildID, found, err := getNoteHexString(notes, "GNU", 0x3) + if err != nil { + return "", fmt.Errorf("could not determine BuildID: %v", err) + } + if !found { + return "", ErrNoBuildID + } + return buildID, nil +} + +// GetSectionAddress returns the address of an ELF section. +// `found` is set to false if such a section does not exist. +func GetSectionAddress(e *elf.File, sectionName string) ( + addr uint64, found bool, err error) { + section := e.Section(sectionName) + if section == nil { + return 0, false, nil + } + + return section.Addr, true, nil +} + +// getNoteHexString returns the hex string contents of an ELF note from a note section, as described +// in the ELF standard in Figure 2-3. +func getNoteHexString(sectionBytes []byte, name string, noteType uint32) ( + noteHexString string, found bool, err error) { + // The data stored inside ELF notes is made of one or multiple structs, containing the + // following fields: + // - namesz // 32-bit, size of "name" + // - descsz // 32-bit, size of "desc" + // - type // 32-bit - 0x3 in case of a BuildID, 0x100 in case of build salt + // - name // namesz bytes, null terminated + // - desc // descsz bytes, binary data: the actual contents of the note + // Because of this structure, the information of the build id starts at the 17th byte. + + // Null terminated string + nameBytes := append([]byte(name), 0x0) + noteTypeBytes := make([]byte, 4) + + binary.LittleEndian.PutUint32(noteTypeBytes, noteType) + noteHeader := append(noteTypeBytes, nameBytes...) // nolint:gocritic + + // Try to find the note in the section + idx := bytes.Index(sectionBytes, noteHeader) + if idx == -1 { + return "", false, nil + } + if idx < 4 { // there needs to be room for descsz + return "", false, fmt.Errorf("could not read note data size") + } + + idxDataStart := idx + len(noteHeader) + idxDataStart += (4 - (idxDataStart & 3)) & 3 // data is 32bit-aligned, round up + + // read descsz and compute the last index of the note data + dataSize := binary.LittleEndian.Uint32(sectionBytes[idx-4 : idx]) + idxDataEnd := uint64(idxDataStart) + uint64(dataSize) + + // Check sanity (64 is totally arbitrary, as we only use it for Linux ID and Build ID) + if idxDataEnd > uint64(len(sectionBytes)) || dataSize > 64 { + return "", false, fmt.Errorf( + "non-sensical note: %d start index: %d, %v end index %d, size %d, section size %d", + idx, idxDataStart, noteHeader, idxDataEnd, dataSize, len(sectionBytes)) + } + return hex.EncodeToString(sectionBytes[idxDataStart:idxDataEnd]), true, nil +} + +func symbolMapFromELFSymbols(syms []elf.Symbol) *libpf.SymbolMap { + symmap := &libpf.SymbolMap{} + for _, sym := range syms { + symmap.Add(libpf.Symbol{ + Name: libpf.SymbolName(sym.Name), + Address: libpf.SymbolValue(sym.Value), + Size: int(sym.Size), + }) + } + symmap.Finalize() + return symmap +} + +// 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) { + syms, err := elfFile.DynamicSymbols() + if err != nil { + return nil, err + } + 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 +} + +// 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()) +} + +// IsGoBinary returns true if the provided file is a Go binary (= an ELF file with +// a known Golang section). +func IsGoBinary(file *elf.File) (bool, error) { + // .go.buildinfo is present since Go 1.13 + sectionFound, err := HasSection(file, ".go.buildinfo") + if sectionFound || err != nil { + return sectionFound, err + } + // Check also .gopclntab, it's present on older Go files, but not on + // Go PIE executables built with new Golang + return HasSection(file, ".gopclntab") +} + +// HasSection returns true if the provided file contains a specific section. +func HasSection(file *elf.File, section string) (bool, error) { + _, sectionFound, err := GetSectionAddress(file, section) + if err != nil { + return false, fmt.Errorf("unable to lookup %v section: %v", section, err) + } + + return sectionFound, nil +} + +// HasCodeSection returns true if the file contains at least one non-empty executable code section. +func HasCodeSection(elfFile *elf.File) bool { + for _, section := range elfFile.Sections { + // NOBITS indicates that the section is actually empty, regardless of the size specified in + // the section header. + // For example, separate debug files generated by objcopy --only-keep-debug do have the same + // section headers as the original file (with the same sizes), including the +x sections. + if section.Type == elf.SHT_NOBITS { + continue + } + + if section.Flags&elf.SHF_EXECINSTR != 0 && section.Size > 0 { + return true + } + } + + return false +} diff --git a/libpf/pfelf/pfelf_test.go b/libpf/pfelf/pfelf_test.go new file mode 100644 index 00000000..86a562b8 --- /dev/null +++ b/libpf/pfelf/pfelf_test.go @@ -0,0 +1,431 @@ +/* + * 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 pfelf_test + +import ( + "debug/elf" + "encoding/hex" + "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" +) + +var ( + // An ELF without DWARF symbols + withoutDebugSymsPath = "testdata/without-debug-syms" + // An ELF with DWARF symbols + withDebugSymsPath = "testdata/with-debug-syms" + // An ELF with only DWARF symbols + separateDebugFile = "testdata/separate-debug-file" +) + +func getELF(path string, t *testing.T) *elf.File { + file, err := elf.Open(path) + assert.Nil(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) + } + 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) + } +} + +func TestGetDebugLink(t *testing.T) { + debugExePath, err := testsupport.WriteTestExecutable1() + if err != nil { + t.Fatalf("Failed to write test executable: %v", 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) + } +} + +func TestGetBuildIDError(t *testing.T) { + debugExePath, err := testsupport.WriteTestExecutable2() + if err != nil { + t.Fatalf("Failed to write test executable: %v", 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) + } +} + +func TestGetDebugLinkError(t *testing.T) { + debugExePath, err := testsupport.WriteTestExecutable2() + if err != nil { + t.Fatalf("Failed to write test executable: %v", 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) + } + }) + } +} + +func TestHasDWARFData(t *testing.T) { + tests := map[string]struct { + filePath string + expectedResult bool + }{ + "ELF executable - no DWARF": {withoutDebugSymsPath, false}, + "ELF executable - with DWARF": {withDebugSymsPath, true}, + "Separate debug symbols": {separateDebugFile, true}, + } + + for testName, testCase := range tests { + name := testName + tc := testCase + t.Run(name, func(t *testing.T) { + elfFile := getELF(tc.filePath, t) + defer elfFile.Close() + + hasDWARF := pfelf.HasDWARFData(elfFile) + + if hasDWARF != tc.expectedResult { + t.Fatalf("Expected %v but got %v", tc.expectedResult, hasDWARF) + } + }) + } +} + +func TestGetSectionAddress(t *testing.T) { + elfFile := getELF("testdata/fixed-address", t) + defer elfFile.Close() + + // 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) + } + + // 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") + } +} + +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) + } +} + +func TestGetKernelVersionBytes(t *testing.T) { + files := []string{"testdata/kernel-image", "testdata/ubuntu-kernel-image"} + for _, f := range files { + f := f + t.Run(f, func(t *testing.T) { + elfFile := getELF(f, 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) + } + }) + } +} + +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) + } + defer os.Remove(exePath) + + ef, err := elf.Open(exePath) + if err != nil { + t.Fatalf("Failed to open test executable: %v", err) + } + defer ef.Close() + + syms, err := pfelf.GetDynamicSymbols(ef) + if err != nil { + t.Fatalf("Failed to get dynamic symbols: %v", err) + } + + assertSymbol(t, syms, "func", 0x1000) + assertSymbol(t, syms, "not_existent", libpf.SymbolValueInvalid) + assertRevSymbol(t, syms, 0x1002, "func", 2) +} + +func testGoBinary(t *testing.T, filename string, isGoExpected bool) { + ef := getELF(filename, t) + defer ef.Close() + + isGo, err := pfelf.IsGoBinary(ef) + assert.Nil(t, err) + assert.Equal(t, isGo, isGoExpected) +} + +func TestIsGoBinary(t *testing.T) { + testGoBinary(t, "testdata/go-binary", true) + testGoBinary(t, "testdata/without-debug-syms", false) +} + +func TestHasCodeSection(t *testing.T) { + tests := map[string]struct { + filePath string + expectedResult bool + }{ + "ELF executable - no DWARF": {withoutDebugSymsPath, true}, + "ELF executable - with DWARF": {withDebugSymsPath, true}, + "Separate debug symbols": {separateDebugFile, false}, + } + + for testName, testCase := range tests { + name := testName + tc := testCase + t.Run(name, func(t *testing.T) { + elfFile := getELF(tc.filePath, t) + defer elfFile.Close() + + hasCode := pfelf.HasCodeSection(elfFile) + + if hasCode != tc.expectedResult { + t.Fatalf("Expected %v but got %v", 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/reference.go b/libpf/pfelf/reference.go new file mode 100644 index 00000000..b39c8730 --- /dev/null +++ b/libpf/pfelf/reference.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 pfelf implements functions for processing of ELF files and extracting data from +// them. This file implements Reference which opens and caches a File on demand. + +package pfelf + +// Reference is a reference to an ELF file which is loaded and cached on demand. +type Reference struct { + // Interface to open ELF files as needed + ELFOpener + + // fileName is the full path of the ELF to open. + fileName string + + // elfFile contains the cached ELF file + elfFile *File +} + +// NewReference returns a new Reference +func NewReference(fileName string, elfOpener ELFOpener) *Reference { + return &Reference{fileName: fileName, ELFOpener: elfOpener} +} + +// FileName returns the file name associated with this Reference +func (ref *Reference) FileName() string { + return ref.fileName +} + +// GetELF returns the File to access this File and keeps it cached. The +// caller of this functions must not Close the File. +func (ref *Reference) GetELF() (*File, error) { + var err error + if ref.elfFile == nil { + ref.elfFile, err = ref.OpenELF(ref.fileName) + } + return ref.elfFile, err +} + +// Close closes the File if it has been opened earlier. +func (ref *Reference) Close() { + if ref.elfFile != nil { + ref.elfFile.Close() + ref.elfFile = nil + } +} diff --git a/libpf/pfelf/testdata/.gitignore b/libpf/pfelf/testdata/.gitignore new file mode 100644 index 00000000..5025847f --- /dev/null +++ b/libpf/pfelf/testdata/.gitignore @@ -0,0 +1,7 @@ +*-debug-syms +fixed-address +the_notorious_build_id +kernel-image +ubuntu-kernel-image +go-binary +separate-debug-file diff --git a/libpf/pfelf/testdata/Makefile b/libpf/pfelf/testdata/Makefile new file mode 100644 index 00000000..5fbb892d --- /dev/null +++ b/libpf/pfelf/testdata/Makefile @@ -0,0 +1,47 @@ +.PHONY: all + +BINARIES=fixed-address \ + go-binary \ + kernel-image \ + separate-debug-file \ + the_notorious_build_id \ + ubuntu-kernel-image \ + with-debug-syms \ + without-debug-syms + +all: $(BINARIES) + +clean: + rm -f $(BINARIES) + +with-debug-syms: test.c + gcc $< -g -o $@ + +without-debug-syms: test.c + gcc $< -s -o $@ + +separate-debug-file: with-debug-syms + objcopy --only-keep-debug $< $@ + +fixed-address: fixed-address.c fixed-address.ld + # The following command will likely print a warning (about a missing -T option), which should be ignored. + # Removing the warning would require passing a fully-fledged linker script to bypass gcc's default. + gcc $^ -o $@ + +# Write an ELF notes file with a build ID +the_notorious_build_id: + # \x04\x00\x00\x00: little endian for 4: the length of "GNU" + null character + # \x14\x00\x00\x00: little endian for 20: the length of "_notorious_build_id_" + # \x03\x00\x00\x00: little endian for 0x3 (Build ID note) + bash -c "echo -en 'somedata\x04\x00\x00\x00\x14\x00\x00\x00\x03\x00\x00\x00GNU\x00_notorious_build_id_\x00somedata' > $@" + +kernel-image: test.c + gcc $< -s -o $@ -DLINUX_VERSION="\"Linux version 1.2.3\\n\"" + +ubuntu-kernel-image: test.c + gcc $< -s -o $@ -DLINUX_VERSION="\"Linux version 1.2.3 (Ubuntu 4.5.6)\\n\"" + +# A fake go binary (with a .gopclntab section) +go-binary: without-debug-syms + objcopy --add-section .gopclntab=/dev/null $< $@ + diff --git a/libpf/pfelf/testdata/fixed-address.c b/libpf/pfelf/testdata/fixed-address.c new file mode 100644 index 00000000..ba6acb8c --- /dev/null +++ b/libpf/pfelf/testdata/fixed-address.c @@ -0,0 +1,8 @@ +__attribute__((section(".coffee_section"))) +int function_at_fixed_address(void) { + return 0; +} + +int main(int argc, char *argv[]) { + return 0; +} diff --git a/libpf/pfelf/testdata/fixed-address.ld b/libpf/pfelf/testdata/fixed-address.ld new file mode 100644 index 00000000..670456e3 --- /dev/null +++ b/libpf/pfelf/testdata/fixed-address.ld @@ -0,0 +1,5 @@ +SECTIONS +{ + . = 0xC0FFEE; + .coffee_section : {} +} diff --git a/libpf/pfelf/testdata/test.c b/libpf/pfelf/testdata/test.c new file mode 100644 index 00000000..fc2b6e0b --- /dev/null +++ b/libpf/pfelf/testdata/test.c @@ -0,0 +1,9 @@ +#ifndef LINUX_VERSION +#define LINUX_VERSION "" +#endif + +const char* version=LINUX_VERSION; // LINUX_VERSION + +int main(int argc, char *argv[]) { + return 0; +} diff --git a/libpf/pfnamespaces/namespaces.go b/libpf/pfnamespaces/namespaces.go new file mode 100644 index 00000000..69434050 --- /dev/null +++ b/libpf/pfnamespaces/namespaces.go @@ -0,0 +1,46 @@ +/* + * 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 pfnamespaces + +import ( + "fmt" + "syscall" + + "go.uber.org/multierr" + + "golang.org/x/sys/unix" +) + +// EnterNamespace enters a new namespace of the specified type, inherited from the provided PID. +// The returned file descriptor must be closed with unix.Close(). +// Note that this function affects the OS thread calling this function, which will likely impact +// more than one goroutine unless you also use runtime.LockOSThread. +func EnterNamespace(pid int, nsType string) (int, error) { + var nsTypeInt int + switch nsType { + case "net": + nsTypeInt = syscall.CLONE_NEWNET + case "uts": + nsTypeInt = syscall.CLONE_NEWUTS + default: + return -1, fmt.Errorf("unsupported namespace type: %s", nsType) + } + + path := fmt.Sprintf("/proc/%d/ns/%s", pid, nsType) + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC, 0) + if err != nil { + return -1, err + } + + err = unix.Setns(fd, nsTypeInt) + if err != nil { + // Close namespace and return the error + return -1, multierr.Combine(err, unix.Close(fd)) + } + + return fd, nil +} diff --git a/libpf/process/coredump.go b/libpf/process/coredump.go new file mode 100644 index 00000000..1b408b85 --- /dev/null +++ b/libpf/process/coredump.go @@ -0,0 +1,561 @@ +/* + * 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. + */ + +// This file implements Process interface to access coredump ELF files. + +// For NT_FILE coredump mappings: https://www.gabriel.urdhr.fr/2015/05/29/core-file/ + +package process + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "fmt" + "hash/fnv" + "io" + "strings" + "unsafe" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" +) + +const ( + // maxNotesSection the maximum section size for notes + maxNotesSection = 16 * 1024 * 1024 +) + +// CoredumpProcess implements Process interface to ELF coredumps +type CoredumpProcess struct { + *pfelf.File + + // files contains coredump's files by name + files map[string]*CoredumpFile + + // pid the original PID of the coredump + pid libpf.PID + + // machineData contains the parsed machine data + machineData MachineData + + // mappings contains the parsed mappings + mappings []Mapping + + // threadInfo contains the parsed thread info + threadInfo []ThreadInfo + + // execPhdrPtr points to the main executable's program headers + execPhdrPtr libpf.Address + + // hasMusl is set if musl c-library is detected in this coredump. This + // is needed when opening ELF inside coredump as musl and glibc have + // differences how they handle the dynamic table. + hasMusl bool +} + +var _ Process = &CoredumpProcess{} + +// CoredumpMapping describes a file backed mapping in a coredump +type CoredumpMapping struct { + // Corresponding PT_LOAD segment + Prog *pfelf.Prog + // File is the backing file for this mapping + File *CoredumpFile + // FileOffset is the offset in the original backing file + FileOffset uint64 +} + +// CoredumpFile contains information about a file mapped into a coredump +type CoredumpFile struct { + // parent is the Coredump inside which this file is + parent *CoredumpProcess + // inode is the synthesized inode for this file + inode uint64 + // Name is the mapped file's name + Name string + // Mappings contains mappings regarding this file + Mappings []CoredumpMapping + // Base is the virtual address where this file is loaded + Base uint64 +} + +// ELF64 Note header. +type Note64 struct { + Namesz, Descsz, Type uint32 +} + +//nolint:revive,stylecheck +const ( + NAMESPACE_CORE = "CORE\x00" + NAMESPACE_LINUX = "LINUX\x00" + + NT_AUXV elf.NType = 6 + NT_FILE elf.NType = 0x46494c45 + NT_ARM_TLS elf.NType = 0x401 + NT_ARM_PAC_MASK elf.NType = 0x406 + + AT_PHDR = 3 + AT_SYSINFO_EHDR = 33 +) + +// getAlignedBytes returns 'size' bytes from source slice, and progresses the +// source slice by 'size' aligned to next 4 byte boundary. Used to parse notes. +func getAlignedBytes(rdr io.Reader, size uint32) ([]byte, error) { + if size == 0 { + return []byte{}, nil + } + alignedSize := (size + 3) &^ 3 + buf := make([]byte, alignedSize) + if n, err := rdr.Read(buf); n != int(alignedSize) || err != nil { + return nil, err + } + return buf[:size], nil +} + +// OpenCoredump opens the named file as a coredump. +func OpenCoredump(name string) (*CoredumpProcess, error) { + f, err := pfelf.Open(name) + if err != nil { + return nil, err + } + return OpenCoredumpFile(f) +} + +// vaddrMappings is internally used during parsing of coredump structures. +// It's the value of a map indexed with mapping virtual address, and contains the data +// needed to associate data from different coredump data structures to proper internals. +type vaddrMappings struct { + // prog is the ELF PT_LOAD Program header for this virtual address + prog *pfelf.Prog + + // mappingIndex is the mapping's index in processState.Mappings + mappingIndex int +} + +// OpenCoredumpFile opens the given `pfelf.File` as a coredump. +// +// Ownership of the file is transferred. Closing the coredump closes the underlying file as well. +func OpenCoredumpFile(f *pfelf.File) (*CoredumpProcess, error) { + cd := &CoredumpProcess{ + File: f, + files: make(map[string]*CoredumpFile), + mappings: make([]Mapping, 0, len(f.Progs)), + threadInfo: make([]ThreadInfo, 0, 8), + } + cd.machineData.Machine = cd.Machine + + vaddrToMappings := make(map[uint64]vaddrMappings) + + // First pass of program headers: get PT_LOAD base addresses. The PT_NOTE header is usually + // before the PT_LOAD ones, so this needs to be done first. + for i := range f.Progs { + p := &f.Progs[i] + if p.Type == elf.PT_LOAD && p.Flags != 0 { + m := Mapping{ + Vaddr: p.Vaddr, + Length: p.Memsz, + Flags: p.Flags, + } + vaddrToMappings[p.Vaddr] = vaddrMappings{ + prog: p, + mappingIndex: len(cd.mappings), + } + cd.mappings = append(cd.mappings, m) + } + } + // Parse the coredump specific PT_NOTE program headers we are interested about. + for i := range f.Progs { + p := &f.Progs[i] + if p.Filesz <= 0 { + continue + } + if p.ProgHeader.Type != elf.PT_NOTE { + continue + } + rdr, err := p.DataReader(maxNotesSection) + if err != nil { + return nil, err + } + var note Note64 + for { + // Read the note header (name and size lengths), followed by reading + // their contents. This code advances the position in 'rdr' and should + // be kept together to parse the notes correctly. + if _, err = rdr.Read(libpf.SliceFrom(¬e)); err != nil { + break + } + var nameBytes, desc []byte + if nameBytes, err = getAlignedBytes(rdr, note.Namesz); err != nil { + break + } + if desc, err = getAlignedBytes(rdr, note.Descsz); err != nil { + break + } + + // Parse the note if we are interested in it (skip others) + name := string(nameBytes) + ty := elf.NType(note.Type) + if name == NAMESPACE_CORE { + switch ty { + case NT_AUXV: + cd.parseAuxVector(desc, vaddrToMappings) + case elf.NT_PRPSINFO: + err = cd.parseProcessInfo(desc) + case elf.NT_PRSTATUS: + err = cd.parseProcessStatus(desc) + case NT_FILE: + err = cd.parseMappings(desc, vaddrToMappings) + } + } else if name == NAMESPACE_LINUX { + switch ty { + case NT_ARM_PAC_MASK: + err = cd.parseArmPacMask(desc) + case NT_ARM_TLS: + err = cd.parseArmTLS(desc) + } + } + + if err != nil { + break + } + } + if err != io.EOF { + return nil, err + } + } + + return cd, nil +} + +// MainExecutable gets the file path from the mappings of the main executable. +func (cd *CoredumpProcess) MainExecutable() string { + if cd.execPhdrPtr == 0 { + return "" + } + + for _, file := range cd.files { + for _, mapping := range file.Mappings { + if cd.execPhdrPtr >= libpf.Address(mapping.Prog.Vaddr) && + cd.execPhdrPtr <= libpf.Address(mapping.Prog.Vaddr+mapping.Prog.Memsz) { + return file.Name + } + } + } + + return "" +} + +// PID implements the Process interface +func (cd *CoredumpProcess) PID() libpf.PID { + return cd.pid +} + +// GetMachineData implements the Process interface +func (cd *CoredumpProcess) GetMachineData() MachineData { + return cd.machineData +} + +// GetMappings implements the Process interface +func (cd *CoredumpProcess) GetMappings() ([]Mapping, error) { + return cd.mappings, nil +} + +// GetThreadInfo implements the Process interface +func (cd *CoredumpProcess) GetThreads() ([]ThreadInfo, error) { + return cd.threadInfo, nil +} + +// OpenMappingFile implements the Process interface +func (cd *CoredumpProcess) OpenMappingFile(_ *Mapping) (ReadAtCloser, error) { + // No filesystem level backing file in coredumps + 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 "" +} + +// CalculateMappingFileID implements the Process interface +func (cd *CoredumpProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, error) { + // It is not possible to calculate the real FileID as the section headers + // are likely missing. So just return a synthesized FileID. + vaddr := make([]byte, 8) + binary.LittleEndian.PutUint64(vaddr, m.Vaddr) + + h := fnv.New128a() + _, _ = h.Write(vaddr) + _, _ = h.Write([]byte(m.Path)) + return libpf.FileIDFromBytes(h.Sum(nil)) +} + +// OpenELF implements the ELFOpener and Process interfaces +func (cd *CoredumpProcess) OpenELF(path string) (*pfelf.File, error) { + // Fallback to directly returning the data from coredump. This comes with caveats: + // + // - The process of loading an ELF binary into memory discards any program regions not marked + // as `PT_LOAD`. This means that we won't be able to read sections like `.debug_lines`. + // - The section table present in memory is typically broken. + // - Writable data sections won't be in their original state. + // + // This essentially means that, during the test run, the HA code is presented with an + // environment that diverges from the environment it operates in when running on a real system + // where the original ELF file is available on disk. However, in order to allow keeping around + // our old test cases from times when we didn't yet bundle the original executables with our + // tests, we allow this fallback. + + if file, ok := cd.files[path]; ok { + return file.OpenELF() + } + return nil, fmt.Errorf("ELF file `%s` not found", path) +} + +// getFile returns (creating if needed) a matching CoredumpFile for given file name +func (cd *CoredumpProcess) getFile(name string) *CoredumpFile { + if cf, ok := cd.files[name]; ok { + return cf + } + if strings.Contains(name, "/ld-musl-") { + cd.hasMusl = true + } + cf := &CoredumpFile{ + parent: cd, + inode: uint64(len(cd.files) + 1), + Name: name, + } + cd.files[name] = cf + return cf +} + +// FileMappingHeader64 is the header for CORE/NT_FILE note +type FileMappingHeader64 struct { + Entries uint64 + PageSize uint64 +} + +// FileMappingEntry64 is the per-mapping data header in CORE/NT_FILE note +type FileMappingEntry64 struct { + Start, End, FileOffset uint64 +} + +// parseMappings processes CORE/NT_FILE note with description of memory mappings +func (cd *CoredumpProcess) parseMappings(desc []byte, + vaddrToMappings map[uint64]vaddrMappings) error { + hdrSize := uint64(unsafe.Sizeof(FileMappingHeader64{})) + entrySize := uint64(unsafe.Sizeof(FileMappingEntry64{})) + + if uint64(len(desc)) < hdrSize { + return fmt.Errorf("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") + } + strs := desc[offs:] + for i := uint64(0); i < hdr.Entries; i++ { + entry := (*FileMappingEntry64)(unsafe.Pointer(&desc[hdrSize+i*entrySize])) + fnlen := bytes.IndexByte(strs, 0) + if fnlen < 0 { + return fmt.Errorf("corrupt NT_FILE: no filename #%d", i+1) + } + + path := trimMappingPath(string(strs[:fnlen])) + cf := cd.getFile(path) + + // In some cases, more than one entry with FO == 0 can exist. This occurs if the first + // section is smaller than a memory page. It is then mapped again as a prefix for the next + // section, presumably to allow the kernel to keep things on-demand paged. We thus pick the + // smallest `Start`. + if entry.FileOffset == 0 && (cf.Base == 0 || entry.Start < cf.Base) { + cf.Base = entry.Start + } + + if m, ok := vaddrToMappings[entry.Start]; ok { + cm := CoredumpMapping{ + Prog: m.prog, + File: cf, + FileOffset: entry.FileOffset * hdr.PageSize, + } + cf.Mappings = append(cf.Mappings, cm) + + mapping := &cd.mappings[m.mappingIndex] + mapping.Path = cf.Name + mapping.FileOffset = entry.FileOffset * hdr.PageSize + // Synthesize non-zero device and inode indicating this is a filebacked mapping + mapping.Device = 1 + mapping.Inode = cf.inode + } + strs = strs[fnlen+1:] + } + return nil +} + +// parseAuxVector processes CORE/NT_AUXV note +func (cd *CoredumpProcess) parseAuxVector(desc []byte, vaddrToMappings map[uint64]vaddrMappings) { + for i := 0; i+16 <= len(desc); i += 16 { + value := binary.LittleEndian.Uint64(desc[i+8:]) + switch binary.LittleEndian.Uint64(desc[i:]) { + case AT_SYSINFO_EHDR: + m, ok := vaddrToMappings[value] + if !ok { + continue + } + + vm := &cd.mappings[m.mappingIndex] + vm.Inode = vdsoInode + vm.Path = vdsoPathName + + cf := cd.getFile(vm.Path) + cm := CoredumpMapping{ + Prog: m.prog, + File: cf, + } + cf.Mappings = append(cf.Mappings, cm) + + case AT_PHDR: + cd.execPhdrPtr = libpf.Address(value) + } + } +} + +// PrpsInfo64 is the 64-bit NT_PRPSINFO note header +type PrpsInfo64 struct { + State uint8 + Sname uint8 + Zombie uint8 + Nice uint8 + Gap uint32 + Flags uint64 + UID uint32 + GID uint32 + PID uint32 + PPID uint32 + PGRP uint32 + SID uint32 + FName [16]byte + Args [80]byte +} + +// parseProcessInfo processes CORE/NT_PRPSINFO note +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) + return nil + } + return fmt.Errorf("unsupported NT_PRPSINFO size: %d", len(desc)) +} + +// parseProcessStatus processes CORE/NT_PRSTATUS note +func (cd *CoredumpProcess) parseProcessStatus(desc []byte) error { + // The corresponding struct definition can be found here: + // https://github.com/torvalds/linux/blob/49d766f3a0e4/include/linux/elfcore.h#L48 + // + // This code just extracts the few bits we are interested in. Because the + // structure varies depending on platform, and we don't want the ELF parser + // to only be able to decode the structure for the host architecture, we + // manually hardcode the struct offsets for each relevant platform instead + // of e.g. using CGO to cast it into a pointer. + // + // The offsets were calculated by running `pahole elf_prstatus` on a machine + // with the corresponding architecture and then pasting the inferred struct + // size and field offsets. + + var sizeof, regStart, regEnd int + switch cd.Machine { + case elf.EM_X86_64: + sizeof = 336 + regStart = 112 + regEnd = 328 + case elf.EM_AARCH64: + sizeof = 392 + regStart = 112 + regEnd = 384 + default: + return fmt.Errorf("unsupported machine: %v", cd.Machine) + } + + if len(desc) != sizeof { + return fmt.Errorf("unsupported NT_PRSTATUS size: %d", len(desc)) + } + + ts := ThreadInfo{ + LWP: binary.LittleEndian.Uint32(desc[32:]), + GPRegs: desc[regStart:regEnd], + } + if cd.Machine == elf.EM_X86_64 { + // Coredump GPRegs on x86_64 is actually "struct user_regs_struct" with + // "struct pt_regs" followed by the segment register data. The fs_base + // is the segment register at index 21. + // See: "struct user_regs_struct" in linux/arch/x86/include/asm/user_64.h for the layout. + ts.TPBase = binary.LittleEndian.Uint64(ts.GPRegs[21*8:]) + } + cd.threadInfo = append(cd.threadInfo, ts) + + return nil +} + +// parseArmPacMask parses the ARM64 specific section containing the PAC masks. +func (cd *CoredumpProcess) parseArmPacMask(desc []byte) error { + // https://github.com/torvalds/linux/blob/1d1df41c5a33/arch/arm64/include/uapi/asm/ptrace.h#L250 + + if len(desc) != 16 { + return fmt.Errorf("unexpected aarch pauth section size %d, expected 16", len(desc)) + } + + cd.machineData.DataPACMask = binary.LittleEndian.Uint64(desc[0:8]) + cd.machineData.CodePACMask = binary.LittleEndian.Uint64(desc[8:16]) + + return nil +} + +// parseArmTLS parses the ARM specific section containing the TLS base address. +func (cd *CoredumpProcess) parseArmTLS(desc []byte) error { + if len(desc) < 8 { + return fmt.Errorf("unexpected aarch tls section size %d, expected at least 8", len(desc)) + } + + numThreads := len(cd.threadInfo) + if numThreads == 0 { + return errors.New("unexpected aarch tls section before NT_PRSTATUS") + } + + // The TLS notes are interleaved between NT_PRSTATUS notes, so + // this fixes up the TPBase of latest seen thread. + cd.threadInfo[numThreads-1].TPBase = binary.LittleEndian.Uint64(desc) + + return nil +} + +// ReadAt reads a file inside a core dump from given file offset. +func (cf *CoredumpFile) ReadAt(p []byte, addr int64) (int, error) { + if len(p) == 0 { + return 0, nil + } + addrEnd := uint64(addr) + uint64(len(p)) + for _, cm := range cf.Mappings { + if uint64(addr) >= cm.FileOffset && + addrEnd <= cm.FileOffset+cm.Prog.Filesz { + return cm.Prog.ReadAt(p, addr-int64(cm.FileOffset)) + } + } + return 0, fmt.Errorf("core does not have data for file '%s' at 0x%x", + cf.Name, addr) +} + +// OpenELF opens the CoredumpFile as an ELF. +// +// The returned `pfelf.File` is borrowing the coredump file. Closing it will not close the +// underlying CoredumpFile. +func (cf *CoredumpFile) OpenELF() (*pfelf.File, error) { + return pfelf.NewFile(cf, cf.Base, cf.parent.hasMusl) +} diff --git a/libpf/process/debug.go b/libpf/process/debug.go new file mode 100644 index 00000000..00567a93 --- /dev/null +++ b/libpf/process/debug.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 process + +import ( + "fmt" + "os" + "runtime" + "strconv" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/remotememory" +) + +type ptraceProcess struct { + systemProcess +} + +var _ Process = &ptraceProcess{} + +func ptraceGetRegset(tid, regset int, data []byte) error { + iovec := unix.Iovec{ + Base: &data[0], + Len: uint64(len(data)), + } + _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, unix.PTRACE_GETREGSET, + uintptr(tid), uintptr(regset), uintptr(unsafe.Pointer(&iovec)), 0, 0) + if errno != 0 { + return fmt.Errorf("ptrace GETREGSET failed with errno %d", errno) + } + + return nil +} + +// NewPtrace attaches the calling goroutine to the target PID using unix +// PTrace API. The goroutine is locked to a system thread due to the PTrace +// API requirements. +// WARNING: All usage of Process interface to this implementation should be +// 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) { + // Lock this goroutine to the OS thread. It is ptrace API requirement + // that all ptrace calls must come from same thread. + runtime.LockOSThread() + + sp := &ptraceProcess{} + sp.pid = pid + sp.remoteMemory = remotememory.RemoteMemory{ReaderAt: sp} + if err := sp.attach(); err != nil { + runtime.UnlockOSThread() + return nil, err + } + return sp, nil +} + +func (sp *ptraceProcess) GetThreads() ([]ThreadInfo, error) { + tidFiles, err := os.ReadDir(fmt.Sprintf("/proc/%d/task", sp.pid)) + if err != nil { + return nil, err + } + + threadInfo := make([]ThreadInfo, 0, len(tidFiles)) + + ti, err := sp.getThreadInfo(int(sp.pid)) + if err != nil { + return nil, err + } + threadInfo = append(threadInfo, ti) + + for _, tidFile := range tidFiles { + if !tidFile.IsDir() { + continue + } + tidNum, err := strconv.ParseInt(tidFile.Name(), 10, 32) + if err != nil { + continue + } + tid := int(tidNum) + // The main thread is handled separately above. + if tid == int(sp.pid) { + continue + } + // Attach to the thread so the state can be queried. + if err = unix.PtraceAttach(tid); err != nil { + continue + } + status := unix.WaitStatus(0) + _, _ = unix.Wait4(tid, &status, 0, nil) + ti, err = sp.getThreadInfo(tid) + _ = unix.PtraceDetach(tid) + if err != nil { + return nil, err + } + threadInfo = append(threadInfo, ti) + } + return threadInfo, nil +} + +func (sp *ptraceProcess) attach() error { + // Attach the main thread + // Per ptrace API, this will send a SIGSTOP to the process + // and suspend the whole process. However, the stopping happens + // asynchronously and needs to be wait for. + if err := unix.PtraceAttach(int(sp.pid)); err != nil { + return err + } + + // Synchronize with process stop. + status := unix.WaitStatus(0) + _, _ = unix.Wait4(int(sp.pid), &status, 0, nil) + + return nil +} + +func (sp *ptraceProcess) ReadAt(p []byte, off int64) (n int, err error) { + return unix.PtracePeekText(int(sp.pid), uintptr(off), p) +} + +func (sp *ptraceProcess) Close() error { + err := unix.PtraceDetach(int(sp.pid)) + runtime.UnlockOSThread() + return err +} diff --git a/libpf/process/debug_amd64.go b/libpf/process/debug_amd64.go new file mode 100644 index 00000000..9d976d8c --- /dev/null +++ b/libpf/process/debug_amd64.go @@ -0,0 +1,29 @@ +//go:build amd64 + +/* + * 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 process + +import ( + "debug/elf" + "encoding/binary" + "fmt" +) + +const currentMachine = elf.EM_X86_64 + +func (sp *ptraceProcess) getThreadInfo(tid int) (ThreadInfo, error) { + prStatus := make([]byte, 28*8) + if err := ptraceGetRegset(tid, int(elf.NT_PRSTATUS), prStatus); err != nil { + return ThreadInfo{}, fmt.Errorf("failed to get LWP %d thread info: %v", tid, err) + } + return ThreadInfo{ + LWP: uint32(tid), + GPRegs: prStatus, + TPBase: binary.LittleEndian.Uint64(prStatus[21*8:]), + }, nil +} diff --git a/libpf/process/debug_arm64.go b/libpf/process/debug_arm64.go new file mode 100644 index 00000000..a4e0ad6b --- /dev/null +++ b/libpf/process/debug_arm64.go @@ -0,0 +1,44 @@ +//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. + * See the file "LICENSE" for details. + */ + +package process + +import ( + "debug/elf" + "encoding/binary" + "fmt" +) + +const currentMachine = elf.EM_AARCH64 + +func (sp *ptraceProcess) GetMachineData() MachineData { + pacMask := make([]byte, 16) + _ = ptraceGetRegset(int(sp.pid), int(NT_ARM_PAC_MASK), pacMask) + + return MachineData{ + Machine: elf.EM_AARCH64, + DataPACMask: binary.LittleEndian.Uint64(pacMask[0:8]), + CodePACMask: binary.LittleEndian.Uint64(pacMask[8:16]), + } +} + +func (sp *ptraceProcess) getThreadInfo(tid int) (ThreadInfo, error) { + prStatus := make([]byte, 35*8) + if err := ptraceGetRegset(tid, int(elf.NT_PRSTATUS), prStatus); err != nil { + return ThreadInfo{}, fmt.Errorf("failed to get LWP %d thread info: %v", tid, err) + } + // Treat TLS base reading error as non-fatal + armTLS := make([]byte, 8) + _ = ptraceGetRegset(tid, int(NT_ARM_TLS), armTLS) + + return ThreadInfo{ + LWP: uint32(tid), + GPRegs: prStatus[:], + TPBase: binary.LittleEndian.Uint64(armTLS), + }, nil +} diff --git a/libpf/process/process.go b/libpf/process/process.go new file mode 100644 index 00000000..26fe8bc3 --- /dev/null +++ b/libpf/process/process.go @@ -0,0 +1,239 @@ +/* + * 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 process + +import ( + "bufio" + "bytes" + "debug/elf" + "errors" + "fmt" + "io" + "os" + "strings" + + "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" +) + +// systemProcess provides an implementation of the Process interface for a +// process that is currently running on this machine. +type systemProcess struct { + pid libpf.PID + + remoteMemory remotememory.RemoteMemory + + fileToMapping map[string]*Mapping +} + +var _ Process = &systemProcess{} + +// New returns an object with Process interface accessing it +func New(pid libpf.PID) Process { + return &systemProcess{ + pid: pid, + remoteMemory: remotememory.NewProcessVirtualMemory(pid), + } +} + +func (sp *systemProcess) PID() libpf.PID { + return sp.pid +} + +func (sp *systemProcess) GetMachineData() MachineData { + return MachineData{Machine: currentMachine} +} + +func trimMappingPath(path string) string { + // Trim the deleted indication from the path. + // See path_with_deleted in linux/fs/d_path.c + path = strings.TrimSuffix(path, " (deleted)") + if path == "/dev/zero" { + // Some JIT engines map JIT area from /dev/zero + // make it anonymous. + return "" + } + return path +} + +func parseMappings(mapsFile io.Reader) ([]Mapping, error) { + mappings := make([]Mapping, 0) + scanner := bufio.NewScanner(mapsFile) + buf := make([]byte, 512) + scanner.Buffer(buf, 8192) + for scanner.Scan() { + var fields [6]string + var addrs [2]string + var devs [2]string + + line := stringutil.ByteSlice2String(scanner.Bytes()) + if stringutil.FieldsN(line, fields[:]) < 5 { + continue + } + if stringutil.SplitN(fields[0], "-", addrs[:]) < 2 { + continue + } + + mapsFlags := fields[1] + if len(mapsFlags) < 3 { + continue + } + flags := elf.ProgFlag(0) + if mapsFlags[0] == 'r' { + flags |= elf.PF_R + } + if mapsFlags[1] == 'w' { + flags |= elf.PF_W + } + if mapsFlags[2] == 'x' { + flags |= elf.PF_X + } + + // Ignore non-executable mappings + if flags&elf.PF_X == 0 { + continue + } + inode := libpf.DecToUint64(fields[4]) + path := fields[5] + if stringutil.SplitN(fields[3], ":", devs[:]) < 2 { + continue + } + device := libpf.HexToUint64(devs[0])<<8 + libpf.HexToUint64(devs[1]) + + if inode == 0 { + if path == "[vdso]" { + // Map to something filename looking with synthesized inode + path = vdsoPathName + device = 0 + inode = vdsoInode + } else if path != "" { + // Ignore [vsyscall] and similar executable kernel + // pages we don't care about + continue + } + } else { + path = trimMappingPath(path) + path = strings.Clone(path) + } + + vaddr := libpf.HexToUint64(addrs[0]) + mappings = append(mappings, Mapping{ + Vaddr: vaddr, + Length: libpf.HexToUint64(addrs[1]) - vaddr, + Flags: flags, + FileOffset: libpf.HexToUint64(fields[2]), + Device: device, + Inode: inode, + Path: path, + }) + } + return mappings, scanner.Err() +} + +// GetMappings will process the mappings file from proc. Additionally, +// a reverse map from mapping filename to a Mapping node is built to allow +// OpenELF opening ELF files using the corresponding proc map_files entry. +// WARNING: This implementation does not support calling GetMappings +// concurrently with itself, or with OpenELF. +func (sp *systemProcess) GetMappings() ([]Mapping, error) { + mapsFile, err := os.Open(fmt.Sprintf("/proc/%d/maps", sp.pid)) + if err != nil { + return nil, err + } + defer mapsFile.Close() + + mappings, err := parseMappings(mapsFile) + if err == nil { + fileToMapping := make(map[string]*Mapping) + for idx := range mappings { + m := &mappings[idx] + if m.Inode != 0 { + fileToMapping[m.Path] = m + } + } + sp.fileToMapping = fileToMapping + } + return mappings, err +} + +func (sp *systemProcess) GetThreads() ([]ThreadInfo, error) { + return nil, errors.New("not implemented") +} + +func (sp *systemProcess) Close() error { + return nil +} + +func (sp *systemProcess) GetRemoteMemory() remotememory.RemoteMemory { + return sp.remoteMemory +} + +func (sp *systemProcess) extractMapping(m *Mapping) (*bytes.Reader, error) { + data := make([]byte, m.Length) + _, err := sp.remoteMemory.ReadAt(data, int64(m.Vaddr)) + if err != nil { + return nil, fmt.Errorf("unable to extract mapping at %#x from PID %d", + m.Vaddr, sp.pid) + } + return bytes.NewReader(data), nil +} + +func (sp *systemProcess) OpenMappingFile(m *Mapping) (ReadAtCloser, error) { + filename := sp.GetMappingFile(m) + if filename == "" { + return nil, fmt.Errorf("no backing file for anonymous memory") + } + return os.Open(filename) +} + +func (sp *systemProcess) GetMappingFile(m *Mapping) string { + if m.IsAnonymous() { + return "" + } + return fmt.Sprintf("/proc/%v/map_files/%x-%x", sp.pid, m.Vaddr, m.Vaddr+m.Length) +} + +// vdsoFileID caches the VDSO FileID. This assumes there is single instance of +// VDSO for the system. +var vdsoFileID libpf.FileID = libpf.UnsymbolizedFileID + +func (sp *systemProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, error) { + if m.IsVDSO() { + if vdsoFileID != libpf.UnsymbolizedFileID { + return vdsoFileID, nil + } + vdso, err := sp.extractMapping(m) + if err != nil { + return libpf.FileID{}, fmt.Errorf("failed to extract VDSO: %v", err) + } + vdsoFileID, err = pfelf.CalculateIDFromReader(vdso) + return vdsoFileID, err + } + return pfelf.CalculateID(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. + if m, ok := sp.fileToMapping[file]; ok { + if m.IsVDSO() { + vdso, err := sp.extractMapping(m) + if err != nil { + return nil, fmt.Errorf("failed to extract VDSO: %v", err) + } + return pfelf.NewFile(vdso, 0, false) + } + ef, err := pfelf.Open(sp.GetMappingFile(m)) + if err == nil { + return ef, nil + } + } + + // Fall back to opening the file using the process specific root + return pfelf.Open(fmt.Sprintf("/proc/%v/root/%s", sp.pid, file)) +} diff --git a/libpf/process/process_test.go b/libpf/process/process_test.go new file mode 100644 index 00000000..a9c98e3b --- /dev/null +++ b/libpf/process/process_test.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 process + +import ( + "debug/elf" + "os" + "strings" + "testing" + + "github.com/elastic/otel-profiling-agent/libpf" + + "github.com/stretchr/testify/assert" +) + +//nolint:lll +var testMappings = `55fe82710000-55fe8273c000 r--p 00000000 fd:01 1068432 /tmp/usr_bin_seahorse +55fe8273c000-55fe827be000 r-xp 0002c000 fd:01 1068432 /tmp/usr_bin_seahorse +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` + +func TestParseMappings(t *testing.T) { + mappings, err := parseMappings(strings.NewReader(testMappings)) + assert.Nil(t, err) + assert.NotNil(t, mappings) + + expected := []Mapping{ + { + Vaddr: 0x55fe8273c000, + Device: 0xfd01, + Flags: elf.PF_R + elf.PF_X, + Inode: 1068432, + Length: 0x82000, + FileOffset: 180224, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x7f63c8c3e000, + Device: 0x0801, + Flags: elf.PF_R + elf.PF_X, + Inode: 1048922, + Length: 0x1A2000, + FileOffset: 544768, + Path: "/tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1", + }, + { + Vaddr: 0x7f63c8ebf000, + Device: 0x1fd01, + Flags: elf.PF_R + elf.PF_X, + Inode: 1075944, + Length: 0x130000, + FileOffset: 114688, + Path: "/tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0", + }, + } + assert.Equal(t, expected, mappings) +} + +func TestNewPIDOfSelf(t *testing.T) { + pr := New(libpf.PID(os.Getpid())) + assert.NotNil(t, pr) + + mappings, err := pr.GetMappings() + assert.Nil(t, err) + assert.Greater(t, len(mappings), 0) +} diff --git a/libpf/process/types.go b/libpf/process/types.go new file mode 100644 index 00000000..de75522e --- /dev/null +++ b/libpf/process/types.go @@ -0,0 +1,127 @@ +/* + * 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. + */ + +// This file defines the interface to access a Process state. + +package process + +import ( + "debug/elf" + "io" + "strings" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/libpf/remotememory" +) + +// vdsoPathName is the path to use for VDSO mappings +const vdsoPathName = "linux-vdso.1.so" + +// vdsoInode is the synthesized inode number for VDSO mappings +const vdsoInode = 50 + +// Mapping contains information about a memory mapping +type Mapping struct { + // Vaddr is the virtual memory start for this mapping + Vaddr uint64 + // Length is the length of the mapping + Length uint64 + // Flags contains the mapping flags and permissions + Flags elf.ProgFlag + // FileOffset contains for file backed mappings the offset from the file start + FileOffset uint64 + // Device holds the device ID where the file is located + Device uint64 + // Inode holds the mapped file's inode number + Inode uint64 + // Path contains the file name for file backed mappings + Path string +} + +func (m *Mapping) IsExecutable() bool { + return m.Flags&elf.PF_X == elf.PF_X +} + +func (m *Mapping) IsAnonymous() bool { + return m.Path == "" || m.IsMemFD() +} + +func (m *Mapping) IsMemFD() bool { + return strings.HasPrefix(m.Path, "/memfd:") +} + +func (m *Mapping) IsVDSO() bool { + return m.Path == vdsoPathName +} + +func (m *Mapping) GetOnDiskFileIdentifier() libpf.OnDiskFileIdentifier { + return libpf.OnDiskFileIdentifier{ + DeviceID: m.Device, + InodeNum: m.Inode, + } +} + +// ThreadInfo contains the information about a thread CPU state needed for unwinding +type ThreadInfo struct { + // TPBase contains the Thread Pointer Base value + TPBase uint64 + // GPRegs contains the CPU state (registers) for the thread + GPRegs []byte + // LWP is the Light Weight Process ID (thread ID) + LWP uint32 +} + +// MachineData contains machine specific information about the process +type MachineData struct { + // Machine is the Process Machine type + Machine elf.Machine + // CodePACMask contains the PAC mask for code pointers. ARM64 specific, otherwise 0. + CodePACMask uint64 + // DataPACMask contains the PAC mask for data pointers. ARM64 specific, otherwise 0. + DataPACMask uint64 +} + +// ReadAtCloser interfaces implements io.ReaderAt and io.Closer +type ReadAtCloser interface { + io.ReaderAt + io.Closer +} + +// Process is the interface to inspect ELF coredump/process. +// The current implementations do not allow concurrent access to this interface +// from different goroutines. As an exception the ELFOpener and the returned +// GetRemoteMemory object are safe for concurrent use. +type Process interface { + // PID returns the process identifier + PID() libpf.PID + + // GetMachineData reads machine specific data from the target process + GetMachineData() MachineData + + // GetMapping reads and parses process memory mappings + GetMappings() ([]Mapping, error) + + // GetThread reads the process thread states + GetThreads() ([]ThreadInfo, error) + + // GetRemoteMemory returns a remote memory reader accessing the target process + GetRemoteMemory() remotememory.RemoteMemory + + // 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 + + // CalculateMappingFileID calculates FileID of the backing file + CalculateMappingFileID(*Mapping) (libpf.FileID, error) + + io.Closer + + pfelf.ELFOpener +} diff --git a/libpf/readatbuf/readatbuf.go b/libpf/readatbuf/readatbuf.go new file mode 100644 index 00000000..9314b910 --- /dev/null +++ b/libpf/readatbuf/readatbuf.go @@ -0,0 +1,178 @@ +/* + * 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. + */ + +// readatbuf providers wrappers adding caching to types that implement the `ReaderAt` interface. + +package readatbuf + +import ( + "fmt" + "io" + + lru "github.com/elastic/go-freelru" + + "github.com/elastic/otel-profiling-agent/libpf/hash" +) + +// page represents a cached region from the underlying reader. +type page struct { + // data contains the data cached from a previous read. + data []byte + // eof determines whether we encountered an EOF when reading the page originally. + eof bool +} + +// Statistics contains statistics about cache efficiency. +type Statistics struct { + Hits uint64 + Misses uint64 + Evictions uint64 +} + +// Reader implements buffering for random access reads via the `ReaderAt` interface. +type Reader struct { + inner io.ReaderAt + cache *lru.LRU[uint, page] + pageSize uint + stats Statistics + sparePageBuf []byte +} + +func HashUInt(v uint) uint32 { + return uint32(hash.Uint64(uint64(v))) +} + +// New creates a new buffered reader supporting random access. The pageSize argument decides the +// size of each region (page) tracked in the cache. cacheSize defines the maximum number of pages +// 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") + } + if cacheSize == 0 { + return nil, fmt.Errorf("cacheSize cannot be zero") + } + + reader = &Reader{ + inner: inner, + pageSize: pageSize, + } + + reader.cache, err = lru.New[uint, page](uint32(cacheSize), HashUInt) + if err != nil { + return nil, fmt.Errorf("failed to create internal cache: %w", err) + } + + reader.cache.SetOnEvict(func(_ uint, page page) { + reader.stats.Evictions++ + // For EOF pages, the slice might have been truncated. However, all slices were originally + // allocated with page size. Thus, we can expand them back to their original size. Perhaps + // counter-intuitively, Go's slice bounds-checking doesn't limit by the length, but by the + // capacity. + reader.sparePageBuf = page.data[:pageSize] + }) + + return +} + +// InvalidateCache flushes the internal cache. Resets the statistics. +func (reader *Reader) InvalidateCache() { + reader.cache.Purge() + reader.stats = Statistics{} +} + +// Statistics returns statistics about cache efficiency. +func (reader *Reader) Statistics() Statistics { + return reader.stats +} + +// ReadAt implements the `ReaderAt` interface. +func (reader *Reader) ReadAt(p []byte, off int64) (int, error) { + if off < 0 { + return 0, fmt.Errorf("negative offset value %d given", off) + } + + // If reading large amounts of data, skip the cache and use inner ReadAt + // directly. This avoids a single read to trash the whole cache. + // When using underlying os.File this also reduces the number of syscalls + // made as the caching logic would split this to ReadAt call per page. + if uint(len(p)) > reader.pageSize*3/2 { + return reader.inner.ReadAt(p, off) + } + + writeOffset := uint(0) + remaining := uint(len(p)) + skipOffset := uint(off) % reader.pageSize + pageIdx := uint(off) / reader.pageSize + + for remaining > 0 { + data, eof, err := reader.getOrReadPage(pageIdx) + if err != nil { + return int(writeOffset), err + } + + copyLen := min(remaining, uint(len(data))-skipOffset) + copy(p[writeOffset:][:copyLen], data[skipOffset:][:copyLen]) + + skipOffset = 0 + pageIdx++ + writeOffset += copyLen + remaining -= copyLen + + if eof { + if remaining == 0 { + // While there was an EOF in the chunk read, the user buffer was small enough to + // not have caused it. + break + } + + // The read is incomplete. + return int(writeOffset), io.EOF + } + } + + return int(writeOffset), nil +} + +func (reader *Reader) getOrReadPage(pageIdx uint) (data []byte, eof bool, err error) { + if cachedPage, exists := reader.cache.Get(pageIdx); exists { + // Data is cached: serve from there. + reader.stats.Hits++ + return cachedPage.data, cachedPage.eof, nil + } + + reader.stats.Misses++ + + var buffer []byte + if reader.sparePageBuf != nil { + // If present, reuse the spare page from previous evictions. + buffer = reader.sparePageBuf + reader.sparePageBuf = nil + } else { + // Otherwise, allocate a fresh one. + buffer = make([]byte, reader.pageSize) + } + + // Read from the underlying reader. + n, err := reader.inner.ReadAt(buffer, int64(pageIdx*reader.pageSize)) + if err != nil { + // We speculatively read more than the original caller asked us to, so running into + // EOF is actually expected for us. + if err == io.EOF { + buffer = buffer[:n] + eof = true + } else { + return nil, false, err + } + } + + if !eof && uint(n) < reader.pageSize { + return nil, false, fmt.Errorf("failed to read whole page") + } + + reader.cache.Add(pageIdx, page{data: buffer, eof: eof}) + return buffer, eof, nil +} diff --git a/libpf/readatbuf/readatbuf_test.go b/libpf/readatbuf/readatbuf_test.go new file mode 100644 index 00000000..cf6fe2d9 --- /dev/null +++ b/libpf/readatbuf/readatbuf_test.go @@ -0,0 +1,32 @@ +/* + * 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 readatbuf_test + +import ( + "bytes" + "testing" + + "github.com/elastic/otel-profiling-agent/libpf/readatbuf" + "github.com/elastic/otel-profiling-agent/testsupport" +) + +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) + } + + testsupport.ValidateReadAtWrapperTransparency(t, 10000, file, cachingReader) +} + +func TestCaching(t *testing.T) { + testVariant(t, 1024, 64, 1) + testVariant(t, 1346, 11, 55) + testVariant(t, 889, 34, 111) +} diff --git a/libpf/remotememory/remotememory.go b/libpf/remotememory/remotememory.go new file mode 100644 index 00000000..4d7af6f4 --- /dev/null +++ b/libpf/remotememory/remotememory.go @@ -0,0 +1,203 @@ +/* + * 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. + */ + +// remotememory provides access to memory space of a process. The ReaderAt +// interface is used for the basic access, and various convenience functions are +// provided to help reading specific data types. +package remotememory + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "golang.org/x/sys/unix" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +// RemoteMemory implements a set of convenience functions to access the remote memory +type RemoteMemory struct { + io.ReaderAt + // Bias is the adjustment for pointers (used to unrelocate pointers in coredump) + Bias libpf.Address +} + +// Valid determines if this RemoteMemory instance contains a valid reference to target process +func (rm RemoteMemory) Valid() bool { + return rm.ReaderAt != nil +} + +// Read fills slice p[] with data from remote memory at address addr +func (rm RemoteMemory) Read(addr libpf.Address, p []byte) error { + _, err := rm.ReadAt(p, int64(addr)) + return err +} + +// Ptr reads a native pointer from remote memory +func (rm RemoteMemory) Ptr(addr libpf.Address) libpf.Address { + var buf [8]byte + if rm.Read(addr, buf[:]) != nil { + return 0 + } + return libpf.Address(binary.LittleEndian.Uint64(buf[:])) - rm.Bias +} + +// Uint8 reads an 8-bit unsigned integer from remote memory +func (rm RemoteMemory) Uint8(addr libpf.Address) uint8 { + var buf [1]byte + if rm.Read(addr, buf[:]) != nil { + return 0 + } + return buf[0] +} + +// Uint16 reads a 16-bit unsigned integer from remote memory +func (rm RemoteMemory) Uint16(addr libpf.Address) uint16 { + var buf [2]byte + if rm.Read(addr, buf[:]) != nil { + return 0 + } + return binary.LittleEndian.Uint16(buf[:]) +} + +// Uint32 reads a 32-bit unsigned integer from remote memory +func (rm RemoteMemory) Uint32(addr libpf.Address) uint32 { + var buf [4]byte + if rm.Read(addr, buf[:]) != nil { + return 0 + } + return binary.LittleEndian.Uint32(buf[:]) +} + +// Uint64 reads a 64-bit unsigned integer from remote memory +func (rm RemoteMemory) Uint64(addr libpf.Address) uint64 { + var buf [8]byte + if rm.Read(addr, buf[:]) != nil { + return 0 + } + return binary.LittleEndian.Uint64(buf[:]) +} + +// String reads a zero terminated string from remote memory +func (rm RemoteMemory) String(addr libpf.Address) string { + buf := make([]byte, 1024) + n, err := rm.ReadAt(buf, int64(addr)) + if n == 0 || (err != nil && err != io.EOF) { + return "" + } + buf = buf[:n] + zeroIdx := bytes.IndexByte(buf, 0) + if zeroIdx >= 0 { + return string(buf[:zeroIdx]) + } + if n != cap(buf) { + return "" + } + + bigBuf := make([]byte, 4096) + copy(bigBuf, buf) + n, err = rm.ReadAt(bigBuf[len(buf):], int64(addr)+int64(len(buf))) + if n == 0 || (err != nil && err != io.EOF) { + return "" + } + bigBuf = bigBuf[:len(buf)+n] + zeroIdx = bytes.IndexByte(bigBuf, 0) + if zeroIdx >= 0 { + return string(bigBuf[:zeroIdx]) + } + + // Not a zero terminated string + return "" +} + +// StringPtr reads a zero terminate string by first dereferencing a string pointer +// from target memory +func (rm RemoteMemory) StringPtr(addr libpf.Address) string { + addr = rm.Ptr(addr) + if addr == 0 { + return "" + } + 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 +} + +func (vm ProcessVirtualMemory) ReadAt(p []byte, off int64) (int, error) { + numBytesWanted := len(p) + if numBytesWanted == 0 { + return 0, nil + } + localIov := []unix.Iovec{{Base: &p[0], Len: uint64(numBytesWanted)}} + remoteIov := []unix.RemoteIovec{{Base: uintptr(off), Len: numBytesWanted}} + numBytesRead, err := unix.ProcessVMReadv(int(vm.pid), localIov, remoteIov, 0) + if err != nil { + err = fmt.Errorf("failed to read PID %v at 0x%x: %w", vm.pid, off, err) + } else if numBytesRead != numBytesWanted { + err = fmt.Errorf("failed to read PID %v at 0x%x: got only %d of %d", + vm.pid, off, numBytesRead, numBytesWanted) + } + return numBytesRead, err +} + +// NewRemoteMemory returns ProcessVirtualMemory implementation of RemoteMemory. +func NewProcessVirtualMemory(pid libpf.PID) RemoteMemory { + return RemoteMemory{ReaderAt: ProcessVirtualMemory{pid}} +} diff --git a/libpf/remotememory/remotememory_test.go b/libpf/remotememory/remotememory_test.go new file mode 100644 index 00000000..f1bf5c36 --- /dev/null +++ b/libpf/remotememory/remotememory_test.go @@ -0,0 +1,65 @@ +/* + * 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 remotememory + +import ( + "bytes" + "errors" + "os" + "reflect" + "syscall" + "testing" + "unsafe" + + "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 RemoteMemTests(t *testing.T, rm RemoteMemory) { + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + dataPtr := libpf.Address(uintptr(unsafe.Pointer(&data[0]))) + str := []byte("this is a string\x00") + strPtr := libpf.Address(uintptr(unsafe.Pointer(&str[0]))) + longStr := append(bytes.Repeat([]byte("long test string"), 4095/16), 0x00) + longStrPtr := libpf.Address(uintptr(unsafe.Pointer(&longStr[0]))) + + 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 + } + } + assertEqual(t, len(rr.GetBuffer()), len(data)-1) +} + +func TestProcessVirtualMemory(t *testing.T) { + RemoteMemTests(t, NewProcessVirtualMemory(libpf.PID(os.Getpid()))) +} diff --git a/libpf/rlimit/rlimit.go b/libpf/rlimit/rlimit.go new file mode 100644 index 00000000..a488f929 --- /dev/null +++ b/libpf/rlimit/rlimit.go @@ -0,0 +1,35 @@ +/* + * 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 rlimit + +import ( + "fmt" + + "golang.org/x/sys/unix" + + log "github.com/sirupsen/logrus" +) + +// MaximizeMemlock updates the memlock resource limit to RLIM_INFINITY. +// It returns a function to reset the resource limit to its original value or an error. +func MaximizeMemlock() (func(), error) { + var oldLimit unix.Rlimit + tmpLimit := unix.Rlimit{ + Cur: unix.RLIM_INFINITY, + Max: unix.RLIM_INFINITY, + } + + if err := unix.Prlimit(0, unix.RLIMIT_MEMLOCK, &tmpLimit, &oldLimit); err != nil { + return nil, fmt.Errorf("failed to set temporary rlimit: %w", err) + } + + return func() { + if err := unix.Setrlimit(unix.RLIMIT_MEMLOCK, &oldLimit); err != nil { + log.Fatalf("Failed to set old rlimit: %v", err) + } + }, nil +} diff --git a/libpf/stringutil/stringutil.go b/libpf/stringutil/stringutil.go new file mode 100644 index 00000000..3c9b806b --- /dev/null +++ b/libpf/stringutil/stringutil.go @@ -0,0 +1,89 @@ +/* + * 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 stringutil + +import ( + "strings" + "unsafe" +) + +var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} + +// FieldsN splits the string s around each instance of one or more consecutive space +// characters, filling f with substrings of s. +// If s contains more fields than n, the last element of f is set to the +// unparsed remainder of s starting with the first non-space character. +// f will stay untouched if s is empty or contains only white space. +// if n is greater than len(f), 0 is returned without doing any parsing. +// +// Apart from the mentioned differences, FieldsN is like an allocation-free strings.Fields. +func FieldsN(s string, f []string) int { + n := len(f) + si := 0 + for i := 0; i < n-1; i++ { + // Find the start of the next field. + for si < len(s) && asciiSpace[s[si]] != 0 { + si++ + } + fieldStart := si + + // Find the end of the field. + for si < len(s) && asciiSpace[s[si]] == 0 { + si++ + } + if fieldStart >= si { + return i + } + + f[i] = s[fieldStart:si] + } + + // Find the start of the next field. + for si < len(s) && asciiSpace[s[si]] != 0 { + si++ + } + + // Put the remainder of s as last element of f. + if si < len(s) { + f[n-1] = s[si:] + return n + } + + return n - 1 +} + +// SplitN splits the string around each instance of sep, filling f with substrings of s. +// If s contains more fields than n, the last element of f is set to the +// unparsed remainder of s starting with the first non-space character. +// f will stay untouched if s is empty or contains only white space. +// if n is greater than len(f), 0 is returned without doing any parsing. +// +// Apart from the mentioned differences, SplitN is like an allocation-free strings.SplitN. +func SplitN(s, sep string, f []string) int { + n := len(f) + i := 0 + for ; i < n-1 && len(s) > 0; i++ { + fieldEnd := strings.Index(s, sep) + if fieldEnd < 0 { + f[i] = s + return i + 1 + } + f[i] = s[:fieldEnd] + s = s[fieldEnd+len(sep):] + } + + // Put the remainder of s as last element of f. + f[i] = s + return i + 1 +} + +// ByteSlice2String converts a byte slice into a string without a heap allocation. +// Be aware that the byte slice and the string share the same memory - which makes +// the string mutable. +func ByteSlice2String(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/libpf/stringutil/stringutil_test.go b/libpf/stringutil/stringutil_test.go new file mode 100644 index 00000000..730485d0 --- /dev/null +++ b/libpf/stringutil/stringutil_test.go @@ -0,0 +1,97 @@ +/* + * 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 stringutil + +import ( + "testing" +) + +func TestFieldsN(t *testing.T) { + tests := map[string]struct { + input string + expected []string + maxFields int + }{ + "empty": {"", []string{}, 2}, + "only spaces": {" ", []string{}, 2}, + "1 field": {"111", []string{"111"}, 2}, + "1 field B": {" 111", []string{"111"}, 2}, + "1 field C": {"111 ", []string{"111"}, 2}, + "1 field D": {" 111 ", []string{"111"}, 2}, + "2 fields": {"111 222", []string{"111", "222"}, 2}, + "3 fields cap 2": {"111 222 333", []string{"111", "222 333"}, 2}, + "3 fields cap 3": {"111 222 333", []string{"111", "222", "333"}, 3}, + "4 fields cap 2": {"111 222 333 444", []string{"111", "222 333 444"}, 2}, + } + + for name, testcase := range tests { + name := name + testcase := testcase + 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) + } + } + }) + } +} + +func TestSplitN(t *testing.T) { + tests := map[string]struct { + input string + expected []string + maxFields int + }{ + "empty": {"", []string{""}, 2}, + "only sep": {"-", []string{"", ""}, 2}, + "1 field": {"111", []string{"111"}, 2}, + "2 fields B": {"-111", []string{"", "111"}, 2}, + "2 fields C": {"111-", []string{"111", ""}, 2}, + "3 fields A": {"-111-", []string{"", "111", ""}, 3}, + "3 fields B": {"111-222", []string{"111", "222"}, 3}, + "4 fields cap 3": {"111-222--333", []string{"111", "222", "-333"}, 3}, + "4 fields cap 4": {"111-222--333", []string{"111", "222", "", "333"}, 4}, + "5 fields cap 3": {"111-222--333-444", []string{"111", "222", "-333-444"}, 3}, + } + + for name, testcase := range tests { + name := name + testcase := testcase + 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) + } + } + }) + } +} + +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) + } + + b[0] = 'a' + if s != "a" { + t.Fatalf("Unexpected string '%s', expected 'a'", s) + } +} diff --git a/libpf/successfailurecounter/successfailurecounter.go b/libpf/successfailurecounter/successfailurecounter.go new file mode 100644 index 00000000..a535b54d --- /dev/null +++ b/libpf/successfailurecounter/successfailurecounter.go @@ -0,0 +1,62 @@ +/* + * 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. + */ + +// successfailurecounter provides a wrapper to atomically increment success or failure counters. +// +// This package is **not** thread safe. Multiple increments to the same SuccessFailureCounter from +// different threads can result in incorrect counter results. +package successfailurecounter + +import ( + "sync/atomic" + + log "github.com/sirupsen/logrus" +) + +// SuccessFailureCounter implements a wrapper to increment success or failure counters exactly once. +type SuccessFailureCounter struct { + success, fail *atomic.Uint64 + sealed bool +} + +// New returns a SuccessFailureCounter that can be incremented exactly once. +func New(success, fail *atomic.Uint64) SuccessFailureCounter { + return SuccessFailureCounter{success: success, fail: fail} +} + +// ReportSuccess increments the success counter or logs an error otherwise. +func (sfc *SuccessFailureCounter) ReportSuccess() { + if sfc.sealed { + log.Errorf("Attempted to report success/failure status more than once.") + return + } + sfc.success.Add(1) + sfc.sealed = true +} + +// ReportFailure increments the failure counter or logs an error otherwise. +func (sfc *SuccessFailureCounter) ReportFailure() { + if sfc.sealed { + log.Errorf("Attempted to report failure/success status more than once.") + return + } + sfc.fail.Add(1) + sfc.sealed = true +} + +// DefaultToSuccess increments the success counter if no counter was updated before. +func (sfc *SuccessFailureCounter) DefaultToSuccess() { + if !sfc.sealed { + sfc.success.Add(1) + } +} + +// DefaultToFailure increments the failure counter if no counter was updated before. +func (sfc *SuccessFailureCounter) DefaultToFailure() { + if !sfc.sealed { + sfc.fail.Add(1) + } +} diff --git a/libpf/successfailurecounter/successfailurecounter_test.go b/libpf/successfailurecounter/successfailurecounter_test.go new file mode 100644 index 00000000..0764ac33 --- /dev/null +++ b/libpf/successfailurecounter/successfailurecounter_test.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 successfailurecounter + +import ( + "sync/atomic" + "testing" +) + +func defaultToSuccess(t *testing.T, sfc SuccessFailureCounter, n int) { + t.Helper() + defer sfc.DefaultToSuccess() + + if n%2 == 0 { + sfc.ReportSuccess() + } else if n%3 == 0 { + sfc.ReportFailure() + } +} + +func defaultToFailure(t *testing.T, sfc SuccessFailureCounter, n int) { + t.Helper() + defer sfc.DefaultToFailure() + + if n%2 == 0 { + sfc.ReportSuccess() + } else if n%3 == 0 { + sfc.ReportFailure() + } +} + +func TestSuccessFailureCounter(t *testing.T) { + tests := map[string]struct { + call func(*testing.T, SuccessFailureCounter, int) + input int + expectedSucess uint64 + expectedFailure uint64 + }{ + "default success - no report": { + call: defaultToSuccess, + input: 1, + expectedSucess: 1, + }, + "default success - report success": { + call: defaultToSuccess, + input: 2, + expectedSucess: 1, + }, + "default success - report failure": { + call: defaultToSuccess, + input: 3, + expectedFailure: 1, + }, + "default failure - no report": { + call: defaultToFailure, + input: 1, + expectedFailure: 1, + }, + "default failure - report success": { + call: defaultToFailure, + input: 2, + expectedSucess: 1, + }, + "default failure - report failure": { + call: defaultToFailure, + input: 3, + expectedFailure: 1, + }, + } + + for name, test := range tests { + name := name + test := test + t.Run(name, func(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()) + } + }) + } +} diff --git a/libpf/symbol.go b/libpf/symbol.go new file mode 100644 index 00000000..96ecb3be --- /dev/null +++ b/libpf/symbol.go @@ -0,0 +1,112 @@ +/* + * 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" + "sort" +) + +// SymbolValue represents the value associated with a symbol, e.g. either an +// offset or an absolute address +type SymbolValue uint64 + +// SymbolName represents the name of a symbol +type SymbolName string + +// SymbolValueInvalid is the value returned by SymbolMap functions when symbol was not found. +const SymbolValueInvalid = SymbolValue(0) + +// SymbolNameUnknown is the value returned by SymbolMap functions when address has no symbol info. +const SymbolNameUnknown = "" + +// SymbolFinder implements a way to find symbol data +type SymbolFinder interface { + LookupSymbol(symbolName SymbolName) (*Symbol, error) + + LookupSymbolAddress(symbolName SymbolName) (SymbolValue, error) +} + +// Symbol represents the name of a symbol +type Symbol struct { + Name SymbolName + Address SymbolValue + Size int +} + +var _ SymbolFinder = &SymbolMap{} + +// SymbolMap represents collections of symbols that can be resolved or reverse mapped +type SymbolMap struct { + nameToSymbol map[SymbolName]*Symbol + addressToSymbol []Symbol +} + +// Add a symbol to the map +func (symmap *SymbolMap) Add(s Symbol) { + symmap.addressToSymbol = append(symmap.addressToSymbol, s) +} + +// Finalize symbol map by sorting and constructing the nameToSymbol table after +// all symbols are inserted via Add() calls +func (symmap *SymbolMap) Finalize() { + sort.Slice(symmap.addressToSymbol, + func(i, j int) bool { + return symmap.addressToSymbol[i].Address > symmap.addressToSymbol[j].Address + }) + symmap.nameToSymbol = make(map[SymbolName]*Symbol, len(symmap.addressToSymbol)) + for i, s := range symmap.addressToSymbol { + symmap.nameToSymbol[s.Name] = &symmap.addressToSymbol[i] + } +} + +// LookupSymbol obtains symbol information. Returns nil and an error if not found. +func (symmap *SymbolMap) LookupSymbol(symbolName SymbolName) (*Symbol, error) { + if sym, ok := symmap.nameToSymbol[symbolName]; ok { + return sym, nil + } + return nil, fmt.Errorf("symbol %v not present in map", symbolName) +} + +// LookupSymbolAddress returns the address of a symbol. +// Returns SymbolValueInvalid and error if not found. +func (symmap *SymbolMap) LookupSymbolAddress(symbolName SymbolName) (SymbolValue, error) { + if sym, ok := symmap.nameToSymbol[symbolName]; ok { + return sym.Address, nil + } + return SymbolValueInvalid, fmt.Errorf("symbol %v not present in map", symbolName) +} + +// LookupByAddress translates the address to a symbolic information. Return empty string and +// absolute address if it did not match any symbol. +func (symmap *SymbolMap) LookupByAddress(val SymbolValue) (SymbolName, Address, bool) { + i := sort.Search(len(symmap.addressToSymbol), + func(i int) bool { + return val >= symmap.addressToSymbol[i].Address + }) + if i < len(symmap.addressToSymbol) && + (symmap.addressToSymbol[i].Size == 0 || + val < symmap.addressToSymbol[i].Address+ + SymbolValue(symmap.addressToSymbol[i].Size)) { + return symmap.addressToSymbol[i].Name, + Address(val - symmap.addressToSymbol[i].Address), + true + } + 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)) { + for _, f := range symmap.nameToSymbol { + cb(f.Name) + } +} + +// Len returns the number of elements in the map. +func (symmap *SymbolMap) Len() int { + return len(symmap.addressToSymbol) +} diff --git a/libpf/testdata/crc32_test_data b/libpf/testdata/crc32_test_data new file mode 100644 index 00000000..72488fc7 --- /dev/null +++ b/libpf/testdata/crc32_test_data @@ -0,0 +1 @@ +crc32_test_data diff --git a/libpf/traceutil/traceutil.go b/libpf/traceutil/traceutil.go new file mode 100644 index 00000000..a76bb48b --- /dev/null +++ b/libpf/traceutil/traceutil.go @@ -0,0 +1,31 @@ +/* + * 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 traceutil + +import ( + "hash/fnv" + "strconv" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +// HashTrace calculates the hash of a trace and returns it. +// Be aware that changes to this calculation will break the ability to +// look backwards for the same TraceHash in our backend. +func HashTrace(trace *libpf.Trace) libpf.TraceHash { + var buf [24]byte + h := fnv.New128a() + for i := uint64(0); i < uint64(len(trace.Files)); i++ { + _, _ = h.Write(trace.Files[i].Bytes()) + // Using FormatUint() or putting AppendUint() into a function leads + // to escaping to heap (allocation). + _, _ = h.Write(strconv.AppendUint(buf[:0], uint64(trace.Linenos[i]), 10)) + } + // make instead of nil avoids a heap allocation + traceHash, _ := libpf.TraceHashFromBytes(h.Sum(make([]byte, 0, 16))) + return traceHash +} diff --git a/libpf/traceutil/traceutil_test.go b/libpf/traceutil/traceutil_test.go new file mode 100644 index 00000000..a4d7ffff --- /dev/null +++ b/libpf/traceutil/traceutil_test.go @@ -0,0 +1,62 @@ +/* + * 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 traceutil + +import ( + "testing" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/support" +) + +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]) + } + } +} + +func TestHashTrace(t *testing.T) { + tests := map[string]struct { + trace *libpf.Trace + result libpf.TraceHash + }{ + "empty trace": { + trace: &libpf.Trace{}, + result: libpf.NewTraceHash(0x6c62272e07bb0142, 0x62b821756295c58d)}, + "python trace": { + trace: &libpf.Trace{ + Linenos: []libpf.AddressOrLineno{0, 1, 2}, + Files: []libpf.FileID{ + libpf.NewFileID(0, 0), + libpf.NewFileID(1, 1), + libpf.NewFileID(2, 2), + }, + FrameTypes: []libpf.FrameType{ + libpf.NativeFrame, + libpf.NativeFrame, + libpf.NativeFrame, + }}, + result: libpf.NewTraceHash(0x21c6fe4c62868856, 0xcf510596eab68dc8)}, + } + + for name, testcase := range tests { + 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) + } + }) + } +} diff --git a/libpf/vc/vc.go b/libpf/vc/vc.go new file mode 100644 index 00000000..a828ad3c --- /dev/null +++ b/libpf/vc/vc.go @@ -0,0 +1,35 @@ +/* + * 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/doc.go b/libpf/xsync/doc.go new file mode 100644 index 00000000..a40e99f7 --- /dev/null +++ b/libpf/xsync/doc.go @@ -0,0 +1,9 @@ +/* + * 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 xsync provides thin wrappers around locking primitives in an effort towards better +// documenting the relationship between locks and the data they protect. +package xsync diff --git a/libpf/xsync/once.go b/libpf/xsync/once.go new file mode 100644 index 00000000..f53f5f52 --- /dev/null +++ b/libpf/xsync/once.go @@ -0,0 +1,68 @@ +/* + * 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 xsync + +import ( + "sync" + "sync/atomic" +) + +// NOTE: synchronization logic closely borrowed from sync.Once + +// Once is a lock that ensures that some data is initialized exactly once. +// +// Does not need explicit construction: simply do Once[MyType]{}. +type Once[T any] struct { + done atomic.Bool + mu sync.Mutex + data T +} + +// GetOrInit the data protected by this lock. +// +// If the init function fails, the error is returned and the data is still +// considered to be uninitialized. The init function will then be called +// again on the next GetOrInit call. Only one thread will ever call init +// at the same time. +func (l *Once[T]) GetOrInit(init func() (T, error)) (*T, error) { + if !l.done.Load() { + // Outlined slow-path to allow inlining of the fast-path. + return l.initSlow(init) + } + + return &l.data, nil +} + +func (l *Once[T]) initSlow(init func() (T, error)) (*T, error) { + l.mu.Lock() + defer l.mu.Unlock() + + // Contending call might have initialized while we waited for the lock. + if l.done.Load() { + return &l.data, nil + } + + var err error + l.data, err = init() + if err != nil { + return nil, err + } + + l.done.Store(true) + return &l.data, err +} + +// Get the previously initialized value. +// +// If the Once is not yet initialized, nil is returned. +func (l *Once[T]) Get() *T { + if !l.done.Load() { + return nil + } + + return &l.data +} diff --git a/libpf/xsync/once_test.go b/libpf/xsync/once_test.go new file mode 100644 index 00000000..c57d723c --- /dev/null +++ b/libpf/xsync/once_test.go @@ -0,0 +1,61 @@ +/* + * 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 xsync_test + +import ( + "errors" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/elastic/otel-profiling-agent/libpf/xsync" + assert "github.com/stretchr/testify/require" +) + +func TestOnceLock(t *testing.T) { + attempt := 0 // intentionally not atomic + once := xsync.Once[string]{} + someError := errors.New("oh no") + numOk := atomic.Uint32{} + wg := sync.WaitGroup{} + + assert.Nil(t, once.Get()) + + for i := 0; i < 32; i++ { + wg.Add(1) + + go func() { + val, err := once.GetOrInit(func() (string, error) { + if attempt == 3 { + time.Sleep(25 * time.Millisecond) + return strconv.Itoa(attempt), nil + } + + attempt++ + return "", someError + }) + + switch err { + case someError: + assert.Nil(t, val) + case nil: + numOk.Add(1) + assert.Equal(t, "3", *val) + default: + assert.Fail(t, "unreachable") + } + + wg.Done() + }() + } + + wg.Wait() + assert.Equal(t, "3", *once.Get()) + assert.Equal(t, uint32(32-3), numOk.Load()) +} diff --git a/libpf/xsync/rwlock.go b/libpf/xsync/rwlock.go new file mode 100644 index 00000000..e81f239d --- /dev/null +++ b/libpf/xsync/rwlock.go @@ -0,0 +1,162 @@ +/* + * 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 xsync + +import "sync" + +// RWMutex is a thin wrapper around sync.RWMutex that hides away the data it protects to ensure it's +// not accidentally accessed without actually holding the lock. +// +// The design is inspired by how Rust implement its locks. +// +// Given Go's weak type system it's not able to provide perfect safety, but it at least clearly +// communicates to developers exactly which resources are protected by which lock without having to +// sift through documentation (or code, if documentation doesn't exist). +// +// To better demonstrate how this abstraction helps to avoid mistakes, consider the following +// example struct implementing an object manager of some sort: +// +// type ID uint64 +// +// type SomeObject struct { +// // ... +// } +// +// type ObjectManager struct { +// objects map[ID]*SomeObject +// } +// +// func (mgr *ObjectManager) AddObject(id ID, obj *SomeObject) { +// mgr.objects[id] = obj +// } +// +// func (mgr *ObjectManager) RemoveObject(id ID) { +// delete(mgr.objects, id) +// } +// +// func (mgr *ObjectManager) GetObject(id ID) *SomeObject { +// x := mgr.objects[id] +// return x +// } +// +// Now you want to rework the public interface of ObjectManager to be thread-safe. The perhaps most +// obvious solution would be to just add `mu sync.RWMutex` to ObjectManager and lock it immediately +// when entering each public function: +// +// type ID uint64 +// +// type SomeObject struct { +// // ... +// } +// +// type ObjectManager struct { +// mu sync.RWMutex +// objects map[ID]*SomeObject +// } +// +// func (mgr *ObjectManager) AddObject(id ID, obj *SomeObject) { +// mgr.mu.Lock() +// mgr.mu.Unlock() // <- oh no, forgot to write `defer`! +// mgr.objects[id] = obj +// } +// +// func (mgr *ObjectManager) RemoveObject(id ID) { +// // oh no, forgot to take the lock entirely! +// delete(mgr.objects, id) +// } +// +// func (mgr *ObjectManager) GetObject(id ID) *SomeObject { +// mgr.mu.RLock() +// defer mgr.mu.RUnlock() +// return mgr.objects[id] +// } +// +// Unfortunately, we made two mistakes in our implementation. The code will however likely still +// pass all kinds of tests, simply because it's very hard to write tests that detect race +// conditions in tests. +// +// Now, the same thing using xsync.RWMutex instead: +// +// type ID uint64 +// +// type SomeObject struct { +// // ... +// } +// +// type ObjectManager struct { +// objects xsync.RWMutex[map[ID]*SomeObject] +// } +// +// func (mgr *ObjectManager) AddObject(id ID, obj *SomeObject) { +// var *SomeObject objects := mgr.objects.RLock() +// mgr.objects.RUnlock(&objects) // <- oh no, forgot to write `defer`! +// objects[id] = obj // <- will immediately crash in tests +// // because `RUnlock` set our pointer to `nil` +// } +// +// func (mgr *ObjectManager) RemoveObject(id ID) { +// // oh no, forgot to take the lock entirely! With xsync.RWMutex, this won't +// // compile: there simply is no direct pointer to the protected data that we +// // could use to accidentally access shared data without going through calling +// // `RLock`/`WLock` first. +// delete(mgr.objects, id) +// } +// +// func (mgr *ObjectManager) GetObject(id ID) *SomeObject { +// objects := mgr.mu.RLock() +// defer mgr.mu.RUnlock(&objects) +// return mgr.objects[id] +// } +type RWMutex[T any] struct { + guarded T + mutex sync.RWMutex +} + +// NewRWMutex creates a new read-write mutex. +func NewRWMutex[T any](guarded T) RWMutex[T] { + return RWMutex[T]{ + guarded: guarded, + } +} + +// RLock locks the mutex for reading, returning a pointer to the protected data. +// +// The caller **must not** write to the data pointed to by the returned pointer. +// +// Further, the caller **must not** let the returned pointer leak out of the scope of the function +// where it was originally created, except for temporarily borrowing it to other functions. The +// caller must make sure that callees never save this pointer anywhere. +func (mtx *RWMutex[T]) RLock() *T { + mtx.mutex.RLock() + return &mtx.guarded +} + +// RUnlock unlocks the mutex after previously being locked by RLock. +// +// Pass a reference to the pointer returned from RLock here to ensure it is invalidated. +func (mtx *RWMutex[T]) RUnlock(ref **T) { + *ref = nil + mtx.mutex.RUnlock() +} + +// WLock locks the mutex for writing, returning a pointer to the protected data. +// +// The caller **must not** let the returned pointer leak out of the scope of the function where it +// was originally created, except for temporarily borrowing it to other functions. The caller must +// make sure that callees never save this pointer anywhere. +func (mtx *RWMutex[T]) WLock() *T { + mtx.mutex.Lock() + return &mtx.guarded +} + +// WUnlock unlocks the mutex after previously being locked by WLock. +// +// Pass a reference to the pointer returned from WLock here to ensure it is invalidated. +func (mtx *RWMutex[T]) WUnlock(ref **T) { + *ref = nil + mtx.mutex.Unlock() +} diff --git a/libpf/xsync/rwlock_test.go b/libpf/xsync/rwlock_test.go new file mode 100644 index 00000000..8a53fae2 --- /dev/null +++ b/libpf/xsync/rwlock_test.go @@ -0,0 +1,78 @@ +/* + * 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 xsync_test + +import ( + "bytes" + "sync/atomic" + "testing" + + "github.com/elastic/otel-profiling-agent/libpf/xsync" + assert "github.com/stretchr/testify/require" +) + +type SharedResourceMutable struct { + somethingThatNeedsLocking uint64 +} + +type SharedResource struct { + mutable xsync.RWMutex[SharedResourceMutable] + atomicStateThatDoesntNeedAdditionalLocking atomic.Uint64 +} + +func TestRWMutex(t *testing.T) { + // Data is split into two halves: the portion that needs locking and the portion that is either + // constant or intrinsically synchronized (e.g. atomics). + sharedResource := SharedResource{ + mutable: xsync.NewRWMutex(SharedResourceMutable{ + somethingThatNeedsLocking: 891723, + }), + atomicStateThatDoesntNeedAdditionalLocking: atomic.Uint64{}, + } + + mutable := sharedResource.mutable.RLock() + mutable.somethingThatNeedsLocking += 123 + sharedResource.mutable.RUnlock(&mutable) + // RUnlock zeros the reference to make sure we can't accidentally use it after unlocking. + assert.Nil(t, mutable) +} + +func TestRWMutex_ReferenceType(t *testing.T) { + buf := bytes.NewBufferString("hello") + + b := xsync.NewRWMutex(buf.Bytes()) + mutable := b.WLock() + *mutable = append(*mutable, []byte("world")...) + b.WUnlock(&mutable) + + afterMutation := b.RLock() + defer b.RUnlock(&afterMutation) + assert.Equal(t, *afterMutation, []byte("helloworld")) +} + +func ExampleRWMutex_WLock() { + m := xsync.NewRWMutex(uint64(0)) + p := m.WLock() + *p = 123 + // Copy the reference, defeating the pointer invalidation in `WUnlock. Do NOT do this. + p2 := p + m.WUnlock(&p) + + // We can incorrectly still write the data without holding the actual lock: + *p2 = 345 +} + +func TestRWMutex_CrashOnUseAfterUnlock(t *testing.T) { + m := xsync.NewRWMutex(uint64(0)) + p := m.WLock() + *p = 123 + m.WUnlock(&p) + + assert.Panics(t, func() { + *p = 345 + }) +} diff --git a/lpm/lpm.go b/lpm/lpm.go new file mode 100644 index 00000000..41e29845 --- /dev/null +++ b/lpm/lpm.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. + */ + +// 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/lpm/lpm_test.go b/lpm/lpm_test.go new file mode 100644 index 00000000..ca52d084 --- /dev/null +++ b/lpm/lpm_test.go @@ -0,0 +1,81 @@ +/* + * 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/maccess/maccess.go b/maccess/maccess.go new file mode 100644 index 00000000..09a89bd0 --- /dev/null +++ b/maccess/maccess.go @@ -0,0 +1,14 @@ +/* + * 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 maccess provides functionality to check if a certain bug in +// copy_from_user_nofault is patched. +// +// There were issues with the Linux kernel function copy_from_user_nofault that +// caused systems to freeze. These issues were fixed with the following patch: +// nolint:lll +// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d319f344561de23e810515d109c7278919bff7b0 +package maccess diff --git a/maccess/maccess_amd64.go b/maccess/maccess_amd64.go new file mode 100644 index 00000000..551335e9 --- /dev/null +++ b/maccess/maccess_amd64.go @@ -0,0 +1,55 @@ +//go:build amd64 + +/* + * 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 maccess + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +// CopyFromUserNoFaultIsPatched tries to find a relative jump instruction in codeblob +// and returns true if this jump based on faultyFuncAddr points to newCheckFuncAddr. +func CopyFromUserNoFaultIsPatched(codeblob []byte, + faultyFuncAddr uint64, newCheckFuncAddr uint64) (bool, error) { + if len(codeblob) == 0 { + return false, fmt.Errorf("empty code blob") + } + + for i := 0; i < len(codeblob); { + idx, offset := getRelativeOffset(codeblob[i:]) + if idx < 0 { + break + } + + // Sanity check: + // Check whether this is a call to `nmi_uaccess_okay`. + // The offset in a relative jump instruction is relative to the start of the next + // instruction (i+idx+5). + if faultyFuncAddr+uint64(i)+uint64(idx)+uint64(offset)+5 == newCheckFuncAddr { + return true, nil + } + + // Start looking for the next relative jump instruction in codeblob after the + // current finding. + i += idx + 1 + } + return false, nil +} + +// getRelativeOffset looks for the E8 call instruction in codeblob and returns the index at which +// this instruction was found first and the relative offset value from this instruction. +func getRelativeOffset(codeblob []byte) (idx int, offset int32) { + idx = bytes.Index(codeblob, []byte{0xe8}) + if idx == -1 || idx+5 > len(codeblob) { + return -1, 0 + } + tmp := binary.LittleEndian.Uint32(codeblob[idx+1:]) + return idx, int32(tmp) +} diff --git a/maccess/maccess_amd64_test.go b/maccess/maccess_amd64_test.go new file mode 100644 index 00000000..6c461614 --- /dev/null +++ b/maccess/maccess_amd64_test.go @@ -0,0 +1,120 @@ +//go:build amd64 + +/* + * 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 maccess + +import "testing" + +// nolint:lll +var codeblobs = map[string]struct { + code []byte + copyFromUserNofaultAddr uint64 + nmiUaccessOkayAddr uint64 + isPatched bool +}{ + "Debian - 6.1.0-13-amd64": { + isPatched: true, + copyFromUserNofaultAddr: 18446744072414051808, // 0xffffffff81283de0 + nmiUaccessOkayAddr: 18446744072411965904, // 0xffffffff810869d0 + code: []byte{ + 0xe8, 0x1b, 0xd4, 0xde, 0xff, // call ffffffff81071200 <__fentry__> + 0x48, 0xb8, 0x00, 0xf0, 0xff, 0xff, 0xff, // movabs $0x7ffffffff000,%rax + 0x7f, 0x00, 0x00, + 0x48, 0x39, 0xd0, // cmp %rdx,%rax + 0x73, 0x0c, // jae ffffffff81283e00 + 0x48, 0xc7, 0xc0, 0xf2, 0xff, 0xff, 0xff, // mov $0xfffffffffffffff2,%rax + 0xe9, 0xc0, 0xdd, 0xb7, 0x00, // jmp ffffffff81e01bc0 <__x86_return_thunk> + 0x48, 0x29, 0xd0, // sub %rdx,%rax + 0x41, 0x55, // push %r13 + 0x41, 0x54, // push %r12 + 0x55, // push %rbp + 0x48, 0x89, 0xf5, // mov %rsi,%rbp + 0x53, // push %rbx + 0x48, 0x89, 0xd3, // mov %rdx,%rbx + 0x48, 0x39, 0xf0, // cmp %rsi,%rax + 0x72, 0x52, // jb ffffffff81283e66 + 0x49, 0x89, 0xfd, // mov %rdi,%r13 + 0xe8, 0xb4, 0x2b, 0xe0, 0xff, // call ffffffff810869d0 + 0x84, 0xc0, // test %al,%al + 0x74, 0x46, // je ffffffff81283e66 + }, + }, + "Amazon Linux - 6.1.56-82.125.amzn2023.x86_64": { + isPatched: true, + copyFromUserNofaultAddr: 18446744071581352080, // 0xffffffff81264090 + nmiUaccessOkayAddr: 18446744071579331344, // 0xffffffff81076b10 + code: []byte{ + 0xe8, 0x6b, 0xe1, 0xdf, 0xff, // call 0xffffffff81062200 + 0x48, 0xb8, 0x00, 0xf0, 0xff, 0xff, 0xff, // movabs $0x7ffffffff000,%rax + 0x7f, 0x00, 0x00, + 0x48, 0x39, 0xc2, // cmp %rax,%rdx + 0x76, 0x0c, // jbe 0xffffffff812640b0 + 0x48, 0xc7, 0xc0, 0xf2, 0xff, 0xff, 0xff, // mov $0xfffffffffffffff2,%rax + 0xe9, 0x90, 0xea, 0xb9, 0x00, // jmp 0xffffffff81e02b40 + 0x48, 0x29, 0xd0, // sub %rdx,%rax + 0x41, 0x55, // push %r13 + 0x41, 0x54, // push %r12 + 0x55, // push %rbp + 0x48, 0x89, 0xf5, // mov %rsi,%rbp + 0x53, // push %rbx + 0x48, 0x89, 0xd3, // mov %rdx,%rbx + 0x48, 0x39, 0xc6, // cmp %rax,%rsi + 0x77, 0x52, // ja 0xffffffff81264116 + 0x49, 0x89, 0xfd, // mov %rdi,%r13 + 0xe8, 0x44, 0x2a, 0xe1, 0xff, // call 0xffffffff81076b10 + 0x84, 0xc0, // test %al,%al + 0x74, 0x46, // je 0xffffffff81264116 + }, + }, + "Debian - 5.19.0": { + // https://snapshot.debian.org/archive/debian/20230501T024743Z/pool/main/l/linux/linux-image-5.19.0-0.deb11.2-cloud-amd64-dbg_5.19.11-1~bpo11%2B1_amd64.deb + isPatched: false, + nmiUaccessOkayAddr: 18446744071579334128, // 0xffffffff810775f0 + copyFromUserNofaultAddr: 18446744071581280176, // 0xffffffff812527b0 + code: []byte{ + 0xe8, 0x0b, 0x07, 0xe1, 0xff, // call ffffffff81062ec0 <__fentry__> + 0x48, 0xb8, 0x00, 0xf0, 0xff, 0xff, 0xff, // movabs $0x7ffffffff000,%rax + 0x7f, 0x00, 0x00, + 0x48, 0x39, 0xc2, // cmp %rax,%rdx + 0x76, 0x0c, // jbe ffffffff812527d0 + 0x48, 0xc7, 0xc0, 0xf2, 0xff, 0xff, 0xff, // mov $0xfffffffffffffff2,%rax + 0xe9, 0x30, 0xf4, 0x9a, 0x00, // jmp ffffffff81c01c00 <__x86_return_thunk> + 0x48, 0x29, 0xd0, // sub %rdx,%rax + 0x41, 0x55, // push %r13 + 0x49, 0x89, 0xf5, // mov %rsi,%r13 + 0x41, 0x54, // push %r12 + 0x55, // push %rbp + 0x53, // push %rbx + 0x48, 0x89, 0xd3, // mov %rdx,%rbx + 0x48, 0x39, 0xc6, // cmp %rax,%rsi + 0x76, 0x12, // jbe ffffffff812527f6 + 0x5b, // pop %rbx + 0x48, 0xc7, 0xc0, 0xf2, 0xff, 0xff, 0xff, // mov $0xfffffffffffffff2,%rax + 0x5d, // pop %rbp + 0x41, 0x5c, // pop %r12 + 0x41, 0x5d, // pop %r13 + }, + }, +} + +func TestGetJumpInCopyFromUserNoFault(t *testing.T) { + for name, test := range codeblobs { + name := name + test := test + 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) + } + }) + } +} diff --git a/maccess/maccess_arm64.go b/maccess/maccess_arm64.go new file mode 100644 index 00000000..fe87feb5 --- /dev/null +++ b/maccess/maccess_arm64.go @@ -0,0 +1,150 @@ +//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. + * See the file "LICENSE" for details. + */ + +package maccess + +import ( + "fmt" + + ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + aa "golang.org/x/arch/arm64/arm64asm" +) + +// Various constants to mark or check for a specific step. +const ( + stepNone = 0 // No instruction marker + stepMov = 1 << iota // Marker for the MOV instruction + stepCmp // Marker for the CMP instruction + stepB // Marker for the B instruction +) + +// CopyFromUserNoFaultIsPatched looks for a set of assembly instructions, that indicate +// that copy_from_user_nofault was patched. +// nmi_uaccess_okay, that was added with [0] to check memory access, is a specific function +// for x86 and returns always TRUE [1] on other architectures like arm64. So the compiler +// optimizes this function as the result of the function is known at compile time. +// +// nolint:lll +// [0] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d319f344561de23e810515d109c7278919bff7b0 +// [1] https://github.com/torvalds/linux/blob/8bc9e6515183935fa0cccaf67455c439afe4982b/include/asm-generic/tlb.h#L26 +func CopyFromUserNoFaultIsPatched(codeblob []byte, _ uint64, _ uint64) (bool, error) { + if len(codeblob) == 0 { + return false, fmt.Errorf("empty code blob") + } + + // With the patch [0] of copy_from_user_nofault, access_ok() got replaced with __access_ok() [1]. + // __access_ok() is an inlined function and returns '(size <= limit) && (addr <= (limit - size))' [2]. + // This function tries to identify the following sequence of instructions in the codeblob: + // MOV X2, #0x1000000000000 + // CMP X19, X2 + // B HI, .+0x14 + // SUB X2, X2, X19 + // + // nolint:lll + // [0] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d319f344561de23e810515d109c7278919bff7b0 + // [1] https://github.com/torvalds/linux/blob/1c41041124bd14dd6610da256a3da4e5b74ce6b1/include/asm-generic/access_ok.h#L20-L41 + // [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 + // Statemachine to keep track of the previously encountered and expected instructions. + var expectedInstructionTracker = stepNone + + for offs := 0; offs < len(codeblob); offs += 4 { + inst, err := aa.Decode(codeblob[offs:]) + if err != nil { + break + } + switch inst.Op { + case aa.MOV: + // Check if an immediate 64-bit value is moved as part of the instruction. + // From the instruction '(size <= limit) && (addr <= (limit - size))', limit comes + // down to TASK_SIZE_MAX, which is usually TASK_SIZE, and is known at compile time. + if v, ok := inst.Args[1].(aa.Imm64); !ok { + continue + } else if ok && v.Imm == 0xfffffffffffffff2 { + // If the immediate value is -EFAULT, ignore this move instruction. + // -EFAULT is the returned error code by copy_from_user_nofault for + // error cases. + continue + } + + if r, ok := inst.Args[0].(aa.Reg); ok { + if regN, ok := ah.Xreg2num(r); ok && + expectedInstructionTracker == stepNone { + trackedReg = regN + expectedInstructionTracker ^= stepMov + continue + } + } + // Reset trackers as the immediate value is not moved into a register + // as expected. + trackedReg = -1 + expectedInstructionTracker = stepNone + case aa.CMP: + if regN, ok := ah.Xreg2num(inst.Args[0]); ok && + expectedInstructionTracker&stepMov == stepMov { + if trackedReg == regN { + expectedInstructionTracker ^= stepCmp + continue + } + } + if regN, ok := ah.Xreg2num(inst.Args[1]); ok { + if trackedReg == regN { + expectedInstructionTracker ^= stepCmp + continue + } + } + // trackedReg is not used in the CMP instruction. + trackedReg = -1 + expectedInstructionTracker = stepNone + case aa.B: + if cond, ok := inst.Args[0].(aa.Cond); ok && + expectedInstructionTracker&(stepMov|stepCmp) == (stepMov|stepCmp) { + if cond.Value == 8 { + // Conditional branching with flag check: C = 1 & Z = 0 + // This is expected after a CMP instruction, which sets flags + // - Z + // 1 if CMP result is zero, indicating an equal result. + // 0 otherwise + // - C + // 1 if CMP results in carry condition, like unsigned overflow + // 0 otherweise + expectedInstructionTracker ^= stepB + continue + } + } + // trackedReg is not used in the B instruction. + trackedReg = -1 + expectedInstructionTracker = stepNone + case aa.SUB: + // If the minuend of the subtraction is trackedReg, copy_from_user_nofault seems + // to be patched. + if regN, ok := ah.Xreg2num(inst.Args[1]); ok { + if trackedReg == regN && expectedInstructionTracker == (stepMov|stepCmp|stepB) { + return true, nil + } + } + // trackedReg is not used in the SUB instruction. + trackedReg = -1 + expectedInstructionTracker = stepNone + case aa.TBNZ: + // Safeguard: + // In unpatched versions of copy_from_user_nofault the 'Test bit and Branch if Nonzero' + // instruction can be found. This instruction originates from the inlined call of + // access_ok(). In patched versions of copy_from_user_nofault, access_ok() got replaced + // with __access_ok(). + return false, nil + default: + trackedReg = -1 + expectedInstructionTracker = stepNone + } + } + + return false, nil +} diff --git a/maccess/maccess_arm64_test.go b/maccess/maccess_arm64_test.go new file mode 100644 index 00000000..8e379583 --- /dev/null +++ b/maccess/maccess_arm64_test.go @@ -0,0 +1,120 @@ +//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. + * See the file "LICENSE" for details. + */ + +package maccess + +import "testing" + +// nolint:lll +var codeblobs = map[string]struct { + code []byte + isPatched bool +}{ + "Debian - 6.1.0-13-arm64": { + isPatched: true, + code: []byte{ + 0x1f, 0x20, 0x03, 0xd5, // nop + 0x1f, 0x20, 0x03, 0xd5, // nop + 0x3f, 0x23, 0x03, 0xd5, // paciasp + 0xfd, 0x7b, 0xbd, 0xa9, // stp x29, x30, [sp, #-48]! + 0xfd, 0x03, 0x00, 0x91, // mov x29, sp + 0xf3, 0x53, 0x01, 0xa9, // stp x19, x20, [sp, #16] + 0xf4, 0x03, 0x01, 0xaa, // mov x20, x1 + 0x21, 0x00, 0xe0, 0xd2, // mov x1, #0x1000000000000 // #281474976710656 + 0x5f, 0x00, 0x01, 0xeb, // cmp x2, x1 + 0xa8, 0x00, 0x00, 0x54, // b.hi ffff8000082b1508 // b.pmore + 0x21, 0x00, 0x02, 0xcb, // sub x1, x1, x2 + 0xf3, 0x03, 0x02, 0xaa, // mov x19, x2 + 0x9f, 0x02, 0x01, 0xeb, // cmp x20, x1 + 0xc9, 0x00, 0x00, 0x54, // b.ls ffff8000082b151c // b.plast + 0xf3, 0x53, 0x41, 0xa9, // ldp x19, x20, [sp, #16] + 0xa0, 0x01, 0x80, 0x92, // mov x0, #0xfffffffffffffff2 // #-14 + }, + }, + "Amazon Linux - 6.1.59-84.139.amzn2023.aarch64": { + isPatched: true, + code: []byte{ + 0xe9, 0x03, 0x1e, 0xaa, // MOV X9, X30 + 0x1f, 0x20, 0x03, 0xd5, // NOP + 0x3f, 0x23, 0x03, 0xd5, // HINT #0x19 + 0xfd, 0x7b, 0xbd, 0xa9, // STP X29, X30, [SP,#-48]! + 0xfd, 0x03, 0x00, 0x91, // MOV X29, SP + 0xf3, 0x53, 0x01, 0xa9, // STP X19, X20, [SP,#16] + 0xf3, 0x03, 0x02, 0xaa, // MOV X19, X2 + 0x22, 0x00, 0xe0, 0xd2, // MOV X2, #0x1000000000000 + 0x7f, 0x02, 0x02, 0xeb, // CMP X19, X2 + 0xa8, 0x00, 0x00, 0x54, // B HI, .+0x14 + 0x42, 0x00, 0x13, 0xcb, // SUB X2, X2, X19 + 0xf4, 0x03, 0x01, 0xaa, // MOV X20, X1 + 0x3f, 0x00, 0x02, 0xeb, // CMP X1, X2 + 0xc9, 0x00, 0x00, 0x54, // B LS, .+0x18 + 0xa0, 0x01, 0x80, 0x92, // MOV X0, #0xfffffffffffffff2 + 0xf3, 0x53, 0x41, 0xa9, // LDP X19, X20, [SP,#16] + }, + }, + "Debian - 5.19.0": { + // https://snapshot.debian.org/archive/debian/20230501T024743Z/pool/main/l/linux/linux-image-5.19.0-0.deb11.2-cloud-arm64-dbg_5.19.11-1~bpo11%2B1_arm64.deb + isPatched: false, + code: []byte{ + 0x1f, 0x20, 0x03, 0xd5, // nop + 0x1f, 0x20, 0x03, 0xd5, // nop + 0x3f, 0x23, 0x03, 0xd5, // paciasp + 0xfd, 0x7b, 0xbd, 0xa9, // stp x29, x30, [sp, #-48]! + 0x03, 0x41, 0x38, 0xd5, // mrs x3, sp_el0 + 0xfd, 0x03, 0x00, 0x91, // mov x29, sp + 0xf3, 0x53, 0x01, 0xa9, // stp x19, x20, [sp, #16] + 0xf3, 0x03, 0x01, 0xaa, // mov x19, x1 + 0xf4, 0x03, 0x02, 0xaa, // mov x20, x2 + 0xf5, 0x5b, 0x02, 0xa9, // stp x21, x22, [sp, #32] + 0xf5, 0x03, 0x00, 0xaa, // mov x21, x0 + 0x64, 0x2c, 0x40, 0xb9, // ldr w4, [x3, #44] + 0x44, 0x05, 0xa8, 0x37, // tbnz w4, #21, ffff80000829eb98 + 0x60, 0x00, 0x40, 0xf9, // ldr x0, [x3] + 0x1f, 0x00, 0x06, 0x72, // tst w0, #0x4000000 + 0xe1, 0x04, 0x00, 0x54, // b.ne ffff80000829eb98 // b.any + }, + }, + "Linux 6.5.11 compiled with LLVM-17": { + // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=v6.5.11&id=799441832db16b99e400ccbec55db801e6992819 + isPatched: true, + code: []byte{ + 0x5f, 0x24, 0x03, 0xd5, // bti c + 0x29, 0x00, 0xe0, 0xd2, // mov x9, #0x1000000000000 // =281474976710656 + 0xa8, 0x01, 0x80, 0x92, // mov x8, #-0xe // =-14 + 0x5f, 0x00, 0x09, 0xeb, // cmp x2, x9 + 0xe8, 0x02, 0x00, 0x54, // b.hi 0x2227b8 <.text+0x2127b8> + 0x29, 0x01, 0x02, 0xcb, // sub x9, x9, x2 + 0x3f, 0x01, 0x01, 0xeb, // cmp x9, x1 + 0x83, 0x02, 0x00, 0x54, // b.lo 0x2227b8 <.text+0x2127b8> + 0x3f, 0x23, 0x03, 0xd5, // paciasp + 0xfd, 0x7b, 0xbe, 0xa9, // stp x29, x30, [sp, #-0x20]! + 0xf3, 0x0b, 0x00, 0xf9, // str x19, [sp, #0x10] + 0xfd, 0x03, 0x00, 0x91, // mov x29, sp + 0x13, 0x41, 0x38, 0xd5, // mrs x19, SP_EL0 + 0x68, 0xae, 0x48, 0xb9, // ldr w8, [x19, #0x8ac] + 0x08, 0x05, 0x00, 0x11, // add w8, w8, #0x1 + 0x68, 0xae, 0x08, 0xb9, // str w8, [x19, #0x8ac] + }, + }, +} + +func TestGetJumpInCopyFromUserNoFault(t *testing.T) { + for name, test := range codeblobs { + name := name + 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) + } + }) + } +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..1146f9fa --- /dev/null +++ b/main.go @@ -0,0 +1,396 @@ +/* + * 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" + "fmt" + "os" + "os/signal" + "runtime" + "time" + + "golang.org/x/sys/unix" + + "github.com/elastic/otel-profiling-agent/host" + hostmeta "github.com/elastic/otel-profiling-agent/hostmetadata/host" + "github.com/elastic/otel-profiling-agent/tracehandler" + + "github.com/elastic/otel-profiling-agent/hostmetadata" + "github.com/elastic/otel-profiling-agent/metrics/reportermetrics" + + "github.com/elastic/otel-profiling-agent/config" + "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/metrics/agentmetrics" + "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/libpf/memorydebug" + "github.com/elastic/otel-profiling-agent/libpf/vc" +) + +// Short copyright / license text for eBPF code +var copyright = `Copyright (C) 2019-2024 Elasticsearch B.V. + +For the eBPF code loaded by Universal Profiling Agent into the kernel, +the following license applies (GPLv2 only). To request a copy of the +GPLv2 code, email us at profiling-feedback@elastic.co. + +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 +GNU General Public License for more details: + +https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html +` + +type exitCode int + +const ( + exitSuccess exitCode = 0 + exitFailure exitCode = 1 + + // Go 'flag' package calls os.Exit(2) on flag parse errors, if ExitOnError is set + exitParseError exitCode = 2 +) + +func startTraceHandling(ctx context.Context, rep reporter.TraceReporter, + times *config.Times, trc *tracer.Tracer) error { + // Spawn monitors for the various result maps + traceCh := make(chan *host.Trace) + + if err := trc.StartMapMonitors(ctx, traceCh); err != nil { + return fmt.Errorf("failed to start map monitors: %v", err) + } + + return tracehandler.Start(ctx, rep, trc, traceCh, times) +} + +func main() { + os.Exit(int(mainWithExitCode())) +} + +func mainWithExitCode() exitCode { + err := parseArgs() + if err != nil { + fmt.Fprintf(os.Stderr, "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", + argMapScaleFactor, maxArgMapScaleFactor) + return exitParseError + } + + if argCopyright { + fmt.Print(copyright) + return exitSuccess + } + + if argVersion { + fmt.Printf("%s\n", vc.Version()) + return exitSuccess + } + + if argBpfVerifierLogLevel > 2 { + fmt.Fprintf(os.Stderr, "invalid eBPF verifier log level: %d", argBpfVerifierLogLevel) + return exitParseError + } + + // Context to drive main goroutine and the Tracer monitors. + mainCtx, mainCancel := signal.NotifyContext(context.Background(), + unix.SIGINT, unix.SIGTERM, unix.SIGABRT) + defer mainCancel() + + // 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 "+ + "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 "+ + "should be between 1 and %d", tracer.ProbabilisticThresholdMax) + return exitParseError + } + + if argVerboseMode { + log.SetLevel(log.DebugLevel) + // Dump the arguments in debug mode. + dumpArgs() + } + + startTime := time.Now() + log.Infof("Starting OTEL profiling agent %s (revision %s, build timestamp %s)", + vc.Version(), vc.Revision(), vc.BuildTimestamp()) + + // Enable dumping of full heaps if the size of the allocated Golang heap + // exceeds 150m, and start dumping memory profiles when the heap exceeds + // 250m (only in debug builds, go build -tags debug). + memorydebug.Init(1024*1024*250, 1024*1024*150) + + if !argNoKernelVersionCheck { + 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) + return exitFailure + } + + var minMajor, minMinor uint32 + switch runtime.GOARCH { + case "amd64": + minMajor, minMinor = 4, 15 + case "arm64": + // Older ARM64 kernel versions have broken bpf_probe_read. + // https://github.com/torvalds/linux/commit/6ae08ae3dea2cfa03dd3665a3c8475c2d429ef47 + minMajor, minMinor = 5, 5 + default: + msg := fmt.Sprintf("unsupported architecture: %s", runtime.GOARCH) + log.Error(msg) + return exitFailure + } + + if major < minMajor || (major == minMajor && minor < minMinor) { + msg := fmt.Sprintf("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) + return exitFailure + } + + if err = tracer.ProbeTracepoint(); err != nil { + msg := fmt.Sprintf("Failed to probe tracepoint: %v", err) + log.Error(msg) + return exitFailure + } + + validatedTags := hostmeta.ValidateTags(argTags) + log.Debugf("Validated tags: %s", validatedTags) + + var presentCores uint16 + presentCores, err = hostmeta.PresentCPUCores() + if err != nil { + msg := fmt.Sprintf("Failed to read CPU file: %v", err) + log.Error(msg) + return exitFailure + } + + // Retrieve host metadata that will be stored with the HA config, and + // 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) + } + + // Metadata retrieval may fail, in which case, we initialize all values + // to the empty string. + for _, hostMetadataKey := range []string{ + hostmeta.KeyIPAddress, + hostmeta.KeyHostname, + hostmeta.KeyKernelVersion, + } { + if _, ok := hostMetadataMap[hostMetadataKey]; !ok { + hostMetadataMap[hostMetadataKey] = "" + } + } + + log.Debugf("Reading the configuration") + conf := config.Config{ + ProjectID: uint32(argProjectID), + CacheDirectory: argCacheDirectory, + EnvironmentType: argEnvironmentType, + MachineID: argMachineID, + SecretToken: argSecretToken, + Tags: argTags, + ValidatedTags: validatedTags, + Tracers: argTracers, + Verbose: argVerboseMode, + DisableTLS: argDisableTLS, + NoKernelVersionCheck: argNoKernelVersionCheck, + UploadSymbols: false, + BpfVerifierLogLevel: argBpfVerifierLogLevel, + BpfVerifierLogSize: argBpfVerifierLogSize, + MonitorInterval: argMonitorInterval, + ReportInterval: argReporterInterval, + SamplesPerSecond: uint16(argSamplesPerSecond), + CollectionAgentAddr: argCollAgentAddr, + ConfigurationFile: argConfigFile, + PresentCPUCores: presentCores, + TraceCacheIntervals: 6, + MapScaleFactor: uint8(argMapScaleFactor), + StartTime: startTime, + IPAddress: hostMetadataMap[hostmeta.KeyIPAddress], + Hostname: hostMetadataMap[hostmeta.KeyHostname], + KernelVersion: hostMetadataMap[hostmeta.KeyKernelVersion], + ProbabilisticInterval: argProbabilisticInterval, + ProbabilisticThreshold: argProbabilisticThreshold, + } + if err = config.SetConfiguration(&conf); err != nil { + msg := fmt.Sprintf("Failed to set configuration: %s", err) + log.Error(msg) + return exitFailure + } + log.Debugf("Done setting configuration") + + times := config.GetTimes() + + log.Debugf("Determining tracers to include") + includeTracers, err := parseTracers(argTracers) + if err != nil { + msg := fmt.Sprintf("Failed to parse the included tracers: %s", err) + log.Error(msg) + return exitFailure + } + + if err = config.GenerateNewHostIDIfNecessary(); err != nil { + msg := fmt.Sprintf("Failed to generate new host ID: %s", err) + log.Error(msg) + return exitFailure + } + + log.Infof("Assigned ProjectID: %d HostID: %d", config.ProjectID(), config.HostID()) + + // Scale the queues that report traces or information related to traces + // with the number of CPUs, the reporting interval and the sample frequencies. + tracesQSize := max(1024, + uint32(runtime.NumCPU()*int(argReporterInterval.Seconds()*2)*argSamplesPerSecond)) + + metadataCollector := hostmetadata.NewCollector(argCollAgentAddr) + + // TODO: Maybe abort execution if (some) metadata can not be collected + hostMetadataMap = metadataCollector.GetHostMetadata() + + if bpfJITEnabled, found := hostMetadataMap["host:sysctl/net.core.bpf_jit_enable"]; found { + if bpfJITEnabled == "0" { + log.Warnf("The BPF JIT is disabled (net.core.bpf_jit_enable = 0). " + + "Enable it to reduce CPU overhead.") + } + } + + // Network operations to CA start here + var rep reporter.Reporter + // Connect to the collection agent + rep, err = reporter.StartOTLP(mainCtx, &reporter.Config{ + CollAgentAddr: argCollAgentAddr, + MaxRPCMsgSize: 33554432, // 32 MiB + ExecMetadataMaxQueue: 1024, + CountsForTracesMaxQueue: tracesQSize, + MetricsMaxQueue: 1024, + FramesForTracesMaxQueue: tracesQSize, + FrameMetadataMaxQueue: tracesQSize, + HostMetadataMaxQueue: 2, + FallbackSymbolsMaxQueue: 1024, + DisableTLS: argDisableTLS, + MaxGRPCRetries: 5, + Times: times, + }) + if err != nil { + msg := fmt.Sprintf("Failed to start reporting: %v", err) + log.Error(msg) + return exitFailure + } + + metrics.SetReporter(rep) + + // Now that we've sent the first host metadata update, start a goroutine to keep sending updates + // regularly. This is required so pf-web-service only needs to query metadata for bounded + // periods of time. + metadataCollector.StartMetadataCollection(mainCtx, rep) + + // Start agent specific metric retrieval and report them every second. + agentMetricCancel, agentErr := agentmetrics.Start(mainCtx, 1*time.Second) + if agentErr != nil { + msg := fmt.Sprintf("Error starting the agent specific "+ + "metric collection: %s", agentErr) + log.Error(msg) + return exitFailure + } + defer agentMetricCancel() + // Start reporter metric reporting with 60 second intervals. + defer reportermetrics.Start(mainCtx, rep, 60*time.Second)() + + // Load the eBPF code and map definitions + trc, err := tracer.NewTracer(mainCtx, rep, times, includeTracers, !argSendErrorFrames) + if err != nil { + msg := fmt.Sprintf("Failed to load eBPF tracer: %s", err) + log.Error(msg) + return exitFailure + } + log.Printf("eBPF tracer loaded") + defer trc.Close() + + now := time.Now() + // Initial scan of /proc filesystem to list currently-active PIDs and have them processed. + 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())) + log.Debug("Completed initial PID listing") + + // Attach our tracer to the perf event + if err := trc.AttachTracer(argSamplesPerSecond); err != nil { + msg := fmt.Sprintf("Failed to attach to perf event: %v", err) + log.Error(msg) + return exitFailure + } + log.Info("Attached tracer program") + + if argProbabilisticThreshold < tracer.ProbabilisticThresholdMax { + trc.StartProbabilisticProfiling(mainCtx, + argProbabilisticInterval, argProbabilisticThreshold) + log.Printf("Enabled probabilistic profiling") + } else { + if err := trc.EnableProfiling(); err != nil { + msg := fmt.Sprintf("Failed to enable perf events: %v", err) + log.Error(msg) + return exitFailure + } + } + + if err := trc.AttachSchedMonitor(); err != nil { + msg := fmt.Sprintf("Failed to attach scheduler monitor: %v", err) + log.Error(msg) + return exitFailure + } + + // This log line is used in our system tests to verify if that the agent has started. So if you + // change this log line update also the system test. + log.Printf("Attached sched monitor") + + if err := startTraceHandling(mainCtx, rep, times, trc); err != nil { + msg := fmt.Sprintf("Failed to start trace handling: %v", err) + log.Error(msg) + return exitFailure + } + + // Block waiting for a signal to indicate the program should terminate + <-mainCtx.Done() + + log.Info("Stop processing ...") + rep.Stop() + + log.Info("Exiting ...") + return exitSuccess +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..3b4522fb --- /dev/null +++ b/main_test.go @@ -0,0 +1,68 @@ +/* + * 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/metrics/.gitignore b/metrics/.gitignore new file mode 100644 index 00000000..a4ec72e4 --- /dev/null +++ b/metrics/.gitignore @@ -0,0 +1 @@ +ids.go diff --git a/metrics/agentmetrics/agent.go b/metrics/agentmetrics/agent.go new file mode 100644 index 00000000..457274ae --- /dev/null +++ b/metrics/agentmetrics/agent.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 agentmetrics implements the fetching and reporting of agent specific metrics. +package agentmetrics + +import ( + "context" + "runtime" + "time" + + "golang.org/x/sys/unix" + + "github.com/elastic/otel-profiling-agent/libpf/periodiccaller" + "github.com/elastic/otel-profiling-agent/metrics" + + log "github.com/sirupsen/logrus" +) + +// rusageTimes holdes time values of a rusage call. +type rusageTimes struct { + // utime represents the user time in usec. + utime unix.Timeval + // stime represents the system time in usec. + stime unix.Timeval +} + +const ( + // rusageSelf is the indicator that we get the rusage + // of the calling process itself. + rusageSelf = 0 +) + +// timeDelta calculates the difference between two time values +// and returns the difference in milliseconds. +func timeDelta(now, prev unix.Timeval) int64 { + secDelta := (now.Sec - prev.Sec) * 1000 + usecDelta := (now.Usec - prev.Usec) / 1000 + return secDelta + usecDelta +} + +// report collects agent specific metrics and forwards these +// to the metrics package for further processing. +func (r *rusageTimes) report() { + nGoRoutines := runtime.NumGoroutine() + + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + + var rusage unix.Rusage + if err := unix.Getrusage(rusageSelf, &rusage); err != nil { + log.Errorf("Failed to fetch Rusage: %v", err) + return + } + + // Get the difference to the previous call of rusage. + deltaStime := timeDelta(rusage.Stime, r.stime) + deltaUtime := timeDelta(rusage.Utime, r.utime) + + // Save the current values of the rusage call. + r.stime = rusage.Stime + r.utime = rusage.Utime + + metrics.AddSlice([]metrics.Metric{ + { + ID: metrics.IDAgentGoRoutines, + Value: metrics.MetricValue(nGoRoutines), + }, + { + ID: metrics.IDAgentHeapAlloc, + Value: metrics.MetricValue(stats.HeapAlloc), + }, + { + ID: metrics.IDAgentUTime, + Value: metrics.MetricValue(deltaUtime), + }, + { + ID: metrics.IDAgentSTime, + Value: metrics.MetricValue(deltaStime), + }, + }) +} + +// Start starts the agent specific metric retrieval and reporting. +func Start(mainCtx context.Context, interval time.Duration) (func(), error) { + var rusage unix.Rusage + if err := unix.Getrusage(rusageSelf, &rusage); err != nil { + log.Errorf("Failed to fetch Rusage: %v", err) + return func() {}, err + } + + prev := rusageTimes{ + utime: rusage.Utime, + stime: rusage.Stime, + } + + ctx, cancel := context.WithCancel(mainCtx) + stopReporting := periodiccaller.Start(ctx, interval, func() { + prev.report() + }) + + return func() { + cancel() + stopReporting() + }, nil +} diff --git a/metrics/agentmetrics/agent_test.go b/metrics/agentmetrics/agent_test.go new file mode 100644 index 00000000..501078b2 --- /dev/null +++ b/metrics/agentmetrics/agent_test.go @@ -0,0 +1,61 @@ +/* + * 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 agentmetrics + +import ( + "testing" + + "golang.org/x/sys/unix" +) + +func TestTimeDelta(t *testing.T) { + tests := map[string]struct { + now unix.Timeval + prev unix.Timeval + delta int64 + }{ + "1000ms": {now: unix.Timeval{ + Sec: 1, + Usec: 0, + }, prev: unix.Timeval{ + Sec: 0, + Usec: 0, + }, delta: 1000}, + "1ms": {now: unix.Timeval{ + Sec: 0, + Usec: 1000, + }, prev: unix.Timeval{ + Sec: 0, + Usec: 0, + }, delta: 1}, + "delta too small": {now: unix.Timeval{ + Sec: 0, + Usec: 500, + }, prev: unix.Timeval{ + Sec: 0, + Usec: 0, + }, delta: 0}, + "998 ms": {now: unix.Timeval{ + Sec: 1, + Usec: 1000, + }, prev: unix.Timeval{ + Sec: 0, + Usec: 3000, + }, delta: 998}, + } + + for name, tc := range tests { + name := name + 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) + } + }) + } +} diff --git a/metrics/genids/main.go b/metrics/genids/main.go new file mode 100644 index 00000000..7b92f30d --- /dev/null +++ b/metrics/genids/main.go @@ -0,0 +1,73 @@ +/* + * 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 ( + "bytes" + "encoding/json" + "fmt" + "os" +) + +type metricDef struct { + Description string `json:"description"` + MetricType string `json:"type"` + Name string `json:"name"` + FieldName string `json:"field"` + ID uint32 `json:"id"` + Obsolete bool `json:"obsolete"` +} + +func main() { + if len(os.Args) < 3 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + input, err := os.ReadFile(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading %s: %v", os.Args[1], err) + os.Exit(1) + } + + var metricDefs []metricDef + if err = json.Unmarshal(input, &metricDefs); err != nil { + fmt.Fprintf(os.Stderr, "Error unmarshaling: %v", err) + os.Exit(1) + } + + var output bytes.Buffer + output.WriteString( + "// Code generated from metrics.json. DO NOT EDIT.\n" + + "\n" + + "package metrics\n" + + "\n" + + "// To add a new metric append an entry to metrics.json. ONLY APPEND !\n" + + "// Then run 'make generate' from the top directory.\n" + + "\n" + + "// Below are the different metric IDs that we currently implement.\n" + + "const (\n") + + for _, m := range metricDefs { + if m.Obsolete { + continue + } + + output.WriteString( + fmt.Sprintf("\n\t// %s\n\tID%s = %d\n", + m.Description, m.Name, m.ID)) + } + + output.WriteString( + "\n\t// max number of ID values, keep this as *last entry*\n" + + fmt.Sprintf("\tIDMax = %d\n)\n", len(metricDefs))) + + if err = os.WriteFile(os.Args[2], output.Bytes(), 0o600); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) + os.Exit(1) + } +} diff --git a/metrics/metrics.go b/metrics/metrics.go new file mode 100644 index 00000000..d8fd8813 --- /dev/null +++ b/metrics/metrics.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 metrics contains the code for receiving and reporting host metrics. +package metrics + +import ( + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/reporter" +) + +var ( + // prevTimestamp holds the timestamp of the buffered metrics + prevTimestamp libpf.UnixTime32 + + // metricsBuffer buffers the metricsBuffer for the timestamp assigned to prevTimestamp + metricsBuffer = make([]Metric, IDMax) + + // metricIDSet is a bitvector used for fast membership operations, to avoid reporting + // the same metric ID multiple times in the same batch + metricIDSet = make([]uint64, 1+(IDMax/64)) + + // nMetrics is the number of the current entries in metricsBuffer + nMetrics int + + // mutex serializes the concurrent calls to AddSlice() + mutex sync.RWMutex +) + +// reporterImpl allows swapping out the global metrics reporter. +// +// nil is a valid value indicating that metrics should be voided. +var reporterImpl reporter.MetricsReporter + +// SetReporter sets the reporter instance used to send out metrics. +func SetReporter(r reporter.MetricsReporter) { + reporterImpl = r +} + +// report converts and reports collected metrics via the reporter package. +func report() { + ids := make([]uint32, nMetrics) + values := make([]int64, nMetrics) + + for i := 0; i < nMetrics; i++ { + ids[i] = uint32(metricsBuffer[i].ID) + values[i] = int64(metricsBuffer[i].Value) + } + + if reporterImpl != nil { + reporterImpl.ReportMetrics(uint32(prevTimestamp), ids, values) + } + + nMetrics = 0 + for idx := range metricIDSet { + metricIDSet[idx] = 0 + } +} + +// AddSlice takes a slice of metrics from a metric provider. +// The function buffers the metrics and returns immediately. +// +// Here we collect all metrics until the timestamp changes. +// We then call report() to report all metrics from the previous timestamp. +// +// |----------------- 1s period -------------| +// |--+--------------------------+-----------|--+--...... +// | | | +// report(),AddSlice(ID1) | | +// AddSlice(ID2) | +// | +// report(),AddSlice(ID1) +// +// 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()) + + mutex.Lock() + defer mutex.Unlock() + + if prevTimestamp != now && nMetrics > 0 { + report() + } + prevTimestamp = now + + if newMetrics == nil { + return + } + + for _, metric := range newMetrics { + if metric.ID <= IDInvalid || metric.ID >= IDMax { + log.Errorf("Metric value %d out of range [%d,%d]- needs investigation", + metric.ID, IDInvalid+1, IDMax-1) + continue + } + + idx := metric.ID / 64 + mask := uint64(1) << (metric.ID % 64) + // Metric IDs 1-7 correspond to CPU/IO/Agent metrics and are scheduled + // for collection every second. This increases the probability that they will + // be collected more than once a second, which would trigger this warning. + // TODO: Remove this when metrics are reworked + if metricIDSet[idx]&mask > 0 { + if metric.ID > 7 { + log.Warnf("Metric ID %d:%v reported multiple times", metric.ID, metric.Value) + } + continue + } + + if nMetrics >= len(metricsBuffer) { + // Should not happen + log.Errorf("AddSlice capped reporting to %d metrics - needs investigation", + len(metricsBuffer)) + continue + } + + metricIDSet[idx] |= mask + metricsBuffer[nMetrics].ID = metric.ID + metricsBuffer[nMetrics].Value = metric.Value + nMetrics++ + } +} + +// Add takes a single metric (id and value) from a metric provider. +// The function buffers the metric and returns immediately. +func Add(id MetricID, value MetricValue) { + AddSlice([]Metric{{id, value}}) +} + +// There are two corner cases that we ignore on purpose to simplify the usage. +// We currently don't run into these two cases, likely we never do. +// +// 1. We *only* collect metrics with large intervals. Let's say we collect only once per hour. +// In this (very special) case, periodiccaller ensures that we report and see the metrics in +// the storage/UI as soon as a new hour begins. Users who accesses the metrics would likely +// assume exactly that. +// Currently, we collect different metrics at 1s intervals. +// +// 2. In case we stop the 'sub package' metrics, we would report the last metrics at least +// one second later instead of leaving them in the buffer. +// Currently, we don't stop metric reporting while running host agent. +// If we do, we possibly don't care for 1s more or less of reported data. +// +// If these assumptions change, we can address that by regularly calling AddSlice() with +// an empty slice. Code can be found in commit 1d01d1ff841891010afaf8d64d4c21a05f19d168 +// and earlier. diff --git a/metrics/metrics.json b/metrics/metrics.json new file mode 100644 index 00000000..35bb9181 --- /dev/null +++ b/metrics/metrics.json @@ -0,0 +1,1827 @@ +[ + { + "description": "Leave out the 0 value. It's an indication of not explicitly initialized variables.", + "type": "counter", + "name": "Invalid", + "field": "", + "id": 0 + }, + { + "description": "CPU Usage: values are 0-100%", + "type": "gauge", + "name": "CPUUsage", + "field": "host.cpu.usage", + "unit": "percent", + "id": 1 + }, + { + "description": "I/O Throughput: values are bytes/s", + "type": "gauge", + "name": "IOThroughput", + "field": "host.io.throughput", + "unit": "byte", + "id": 2 + }, + { + "description": "I/O Duration: values are 'weighted # of milliseconds doing I/O'", + "type": "gauge", + "name": "IODuration", + "field": "host.io.duration", + "unit": "ms", + "id": 3 + }, + { + "description": "Absolute number of goroutines when the metric was collected.", + "type": "gauge", + "name": "AgentGoRoutines", + "field": "agent.goroutines", + "id": 4 + }, + { + "description": "Absolute number in bytes of allocated heap objects of the agent.", + "type": "gauge", + "name": "AgentHeapAlloc", + "field": "agent.heap.alloc", + "unit": "byte", + "id": 5 + }, + { + "description": "Difference to previous user CPU time of the agent in Milliseconds.", + "type": "counter", + "name": "AgentUTime", + "field": "agent.time.cpu.user", + "unit": "ms", + "id": 6 + }, + { + "description": "Difference to previous system CPU time of the agent in Milliseconds.", + "type": "counter", + "name": "AgentSTime", + "field": "agent.time.cpu.sys", + "unit": "ms", + "id": 7 + }, + { + "description": "Number of calls to interpreter unwinding in dispatch_interpreters()", + "type": "counter", + "name": "UnwindCallInterpreter", + "field": "bpf.interpreter.calls", + "id": 8 + }, + { + "obsolete": true, + "description": "Number of failures to call interpreter unwinding in dispatch_interpreters()", + "type": "counter", + "name": "UnwindErrCallInterpreter", + "id": 9 + }, + { + "description": "Unwind attempts since the previous check", + "type": "counter", + "name": "UnwindNativeAttempts", + "field": "bpf.native.attempts", + "id": 10 + }, + { + "description": "Unwound frames since the previous check", + "type": "counter", + "name": "UnwindNativeFrames", + "field": "bpf.native.frames", + "id": 11 + }, + { + "description": "Number of times MAX_FRAME_UNWINDS has been exceeded in unwind_next_frame()", + "type": "counter", + "name": "UnwindErrStackLengthExceeded", + "field": "bpf.errors.stack_length_exceeded", + "id": 12 + }, + { + "description": "Number of failed range searches within 20 steps in get_stack_delta()", + "type": "counter", + "name": "UnwindNativeErrLookupTextSection", + "field": "bpf.native.errors.lookup_text_section", + "id": 13 + }, + { + "description": "Number of failures to get stack_unwind_info from big_stack_deltas in get_stack_delta()", + "type": "counter", + "name": "UnwindNativeErrLookupIterations", + "field": "bpf.native.errors.lookup_iterations", + "id": 14 + }, + { + "description": "Number of failures to get stack_unwind_info from big_stack_deltas in get_stack_delta()", + "type": "counter", + "name": "UnwindNativeErrLookupRange", + "field": "bpf.native.errors.lookup_range", + "id": 15 + }, + { + "description": "Number of kernel addresses passed to get_text_section()", + "type": "counter", + "name": "UnwindNativeErrKernelAddress", + "field": "bpf.native.errors.kernel_address", + "id": 16 + }, + { + "description": "Number of failures to find the text section in get_text_section()", + "type": "counter", + "name": "UnwindNativeErrWrongTextSection", + "field": "bpf.native.errors.wrong_text_section", + "id": 17 + }, + { + "description": "Number of failures due to PC == 0 in unwind_next_frame()", + "type": "counter", + "name": "UnwindErrZeroPC", + "field": "bpf.errors.zero_pc", + "id": 18 + }, + { + "description": "Number of attempted python unwinds", + "type": "counter", + "name": "UnwindPythonAttempts", + "field": "bpf.python.attempts", + "id": 19 + }, + { + "description": "Number of unwound python frames", + "type": "counter", + "name": "UnwindPythonFrames", + "field": "bpf.python.frames", + "id": 20 + }, + { + "description": "Number of failures to read from pyinfo->pyThreadStateCurrentAddr", + "type": "counter", + "name": "UnwindPythonErrBadPyThreadStateCurrentAddr", + "field": "bpf.python.errors.bad_py_thread_state_current_addr", + "id": 21 + }, + { + "description": "Number of PyThreadState being 0x0", + "type": "counter", + "name": "UnwindPythonErrZeroThreadState", + "field": "bpf.python.errors.zero_thread_state", + "id": 22 + }, + { + "obsolete": true, + "description": "Number of failures to read the autoTLSkey address", + "type": "counter", + "name": "UnwindPythonErrBadAutoTLSKeyAddr", + "id": 23 + }, + { + "description": "Number of failures to read from the TLS", + "type": "counter", + "name": "UnwindErrBadTLSAddr", + "field": "bpf.errors.bad_tls_addr", + "id": 24 + }, + { + "obsolete": true, + "description": "Number of failures to lookup the fsbase offset in tls_get_base()", + "type": "counter", + "name": "UnwindErrLookupFSBaseOffset", + "id": 25 + }, + { + "description": "Number of failures to get the TLS base in tls_get_base()", + "type": "counter", + "name": "UnwindErrBadTPBaseAddr", + "field": "bpf.errors.bad_tp_base_addr", + "id": 26 + }, + { + "description": "Number of failures to read PyThreadState.frame in unwind_python()", + "type": "counter", + "name": "UnwindPythonErrBadThreadStateFrameAddr", + "field": "bpf.python.errors.bad_thread_state_frame_addr", + "id": 27 + }, + { + "obsolete": true, + "description": "Number of failures to read PyFrameObject->f_back in walk_python_stack()", + "type": "counter", + "name": "UnwindPythonErrBadFrameObjectBackAddr", + "field": "bpf.python.errors.bad_frame_object_back_addr", + "id": 28 + }, + { + "obsolete": true, + "description": "Number of failures to read PyFrameObject->f_code in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadFrameCodeObjectAddr", + "field": "bpf.python.errors.bad_frame_code_object_addr", + "id": 29 + }, + { + "description": "Number of NULL code objects found in process_python_frame()", + "type": "counter", + "name": "UnwindPythonZeroFrameCodeObject", + "field": "bpf.python.zero_frame_code_object", + "id": 30 + }, + { + "obsolete": true, + "description": "Number of code objects with no filename in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadCodeObjectFilenameAddr", + "id": 31 + }, + { + "obsolete": true, + "description": "Number of failures to zero out filename in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadZeroFileAddr", + "id": 32 + }, + { + "obsolete": true, + "description": "Number of failures to get the file ID in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadFilenameAddr", + "id": 33 + }, + { + "obsolete": true, + "description": "Number of failures to get the file ID in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrNoFileID", + "id": 34 + }, + { + "obsolete": true, + "description": "Number of failures to get the last instruction address in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadFrameLastInstructionAddr", + "field": "bpf.python.errors.bad_frame_last_instruction_addr", + "id": 35 + }, + { + "description": "Number of failures to get code object's argcount in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadCodeObjectArgCountAddr", + "field": "bpf.python.errors.bad_code_object_arg_count_addr", + "id": 36 + }, + { + "obsolete": true, + "description": "Number of failures to get code object's kwonlyargcount in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadCodeObjectKWOnlyArgCountAddr", + "field": "bpf.python.errors.bad_code_object_kw_only_arg_count_addr", + "id": 37 + }, + { + "obsolete": true, + "description": "Number of failures to get code object's flags in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadCodeObjectFlagsAddr", + "field": "bpf.python.errors.bad_code_object_flags_addr", + "id": 38 + }, + { + "obsolete": true, + "description": "Number of failures to get code object's first line number in process_python_frame()", + "type": "counter", + "name": "UnwindPythonErrBadCodeObjectFirstLineNumberAddr", + "field": "bpf.python.errors.bad_code_object_first_line_number_addr", + "id": 39 + }, + { + "obsolete": true, + "description": "Current size of the hash map mmap_monitor", + "type": "gauge", + "name": "HashmapMmapMonitor", + "id": 40 + }, + { + "obsolete": true, + "description": "Current size of the hash map mmap_executable", + "type": "gauge", + "name": "HashmapMmapExecutable", + "id": 41 + }, + { + "obsolete": true, + "description": "Current size of the hash map mprotect_executable", + "type": "gauge", + "name": "HashmapMprotectExecutable", + "id": 42 + }, + { + "description": "The number of executables loaded to eBPF maps", + "type": "gauge", + "name": "NumExeIDLoadedToEBPF", + "field": "agent.num_exe_id_loaded_to_ebpf", + "id": 43 + }, + { + "description": "Current size of the hash map pid_page_to_mapping_info", + "type": "gauge", + "name": "HashmapPidPageToMappingInfo", + "field": "agent.hashmap_pid_page_to_mapping_info.size", + "id": 44 + }, + { + "obsolete": true, + "description": "Number of failures to create hash for a trace in update_trace_count()", + "type": "counter", + "name": "ErrHashTrace", + "field": "bpf.errors.hash_trace", + "id": 45 + }, + { + "obsolete": true, + "description": "Number of failures to report new frames", + "type": "counter", + "name": "ErrReportNewFrames", + "field": "bpf.errors.report_new_frames", + "id": 46 + }, + { + "description": "Number of invalid stack deltas in the native unwinder", + "type": "counter", + "name": "UnwindNativeErrStackDeltaInvalid", + "field": "bpf.native.errors.stack_delta_invalid", + "id": 47 + }, + { + "description": "Number of times unwind_stop is called without a trace", + "type": "counter", + "name": "ErrEmptyStack", + "field": "bpf.errors.empty_stack", + "id": 48 + }, + { + "description": "Current size of the hash map pycodeobject_to_fileid", + "type": "gauge", + "name": "HashmapPyCodeObjectToFileID", + "field": "agent.hashmap_py_code_object_to_file_id.size", + "id": 49 + }, + { + "description": "Number of attempted Hotspot frame unwinds", + "type": "counter", + "name": "UnwindHotspotAttempts", + "field": "bpf.hotspot.attempts", + "id": 50 + }, + { + "description": "Number of unwound Hotspot frames", + "type": "counter", + "name": "UnwindHotspotFrames", + "field": "bpf.hotspot.frames", + "id": 51 + }, + { + "description": "Number of failures to get hotspot codeblob address (no heap or bad segmap)", + "type": "counter", + "name": "UnwindHotspotErrNoCodeblob", + "field": "bpf.hotspot.errors.no_codeblob", + "id": 52 + }, + { + "description": "Number of failures to get codeblob data", + "type": "counter", + "name": "UnwindHotspotErrInvalidCodeblob", + "field": "bpf.hotspot.errors.invalid_codeblob", + "id": 53 + }, + { + "description": "Number of failures to unwind interpreter due to invalid FP", + "type": "counter", + "name": "UnwindHotspotErrInterpreterFP", + "field": "bpf.hotspot.errors.interpreter_fp", + "id": 54 + }, + { + "description": "Number of successfully symbolized python frames", + "type": "counter", + "name": "PythonSymbolizationSuccesses", + "field": "agent.python.symbolization.successes", + "id": 55 + }, + { + "description": "Number of Python frames that failed symbolization", + "type": "counter", + "name": "PythonSymbolizationFailures", + "field": "agent.python.symbolization.failures", + "id": 56 + }, + { + "description": "Number of successfully symbolized hotspot frames", + "type": "counter", + "name": "HotspotSymbolizationSuccesses", + "field": "agent.hotspot.symbolization.successes", + "id": 57 + }, + { + "description": "Number of Hotspot frames that failed symbolization", + "type": "counter", + "name": "HotspotSymbolizationFailures", + "field": "agent.hotspot.symbolization.failures", + "id": 58 + }, + { + "description": "Number of times that PC hold a value smaller than 0x1000", + "type": "counter", + "name": "UnwindNativeSmallPC", + "field": "bpf.native.small_pc", + "id": 59 + }, + { + "description": "Indicator for whether the exeMetadata queue has been overwritten", + "type": "counter", + "name": "ExeMetadataOverwrite", + "field": "agent.overwrites.exe_metadata", + "id": 60 + }, + { + "description": "Indicator for whether the countsForTraces queue has been overwritten", + "type": "counter", + "name": "CountsForTracesOverwrite", + "field": "agent.overwrites.counts_for_traces", + "id": 61 + }, + { + "description": "Indicator for whether the metrics queue has been overwritten", + "type": "counter", + "name": "MetricsOverwrite", + "field": "agent.overwrites.metrics", + "id": 62 + }, + { + "description": "Indicator for whether the framesForTraces queue has been overwritten", + "type": "counter", + "name": "FramesForTracesOverwrite", + "field": "agent.overwrites.frames_for_traces", + "id": 63 + }, + { + "description": "Indicator for whether the frameMetadata queue has been overwritten", + "type": "counter", + "name": "FrameMetadataOverwrite", + "field": "agent.overwrites.frame_metadata", + "id": 64 + }, + { + "description": "Indicator for whether the hostMetadata queue has been overwritten", + "type": "counter", + "name": "HostMetadataOverwrite", + "field": "agent.overwrites.host_metadata", + "id": 65 + }, + { + "description": "Indicator for whether the fallbackSymbols queue has been overwritten", + "type": "counter", + "name": "FallbackSymbolsOverwrite", + "field": "agent.overwrites.fallback_symbols", + "id": 66 + }, + { + "description": "Number of lost perf events in the communication between kernel and user space (report_events)", + "type": "counter", + "name": "PerfEventLost", + "field": "agent.errors.perf_event_lost", + "id": 67 + }, + { + "description": "Number of stop stack deltas in the native unwinder (success)", + "type": "counter", + "name": "UnwindNativeStackDeltaStop", + "field": "bpf.native.stack_delta_stop", + "id": 68 + }, + { + "description": "Number of times failure to read PC from unwound stack (invalid stack delta)", + "type": "counter", + "name": "UnwindNativeErrPCRead", + "field": "bpf.native.errors.pc_read", + "id": 69 + }, + { + "description": "Number of times that a lookup of a inner map for stack deltas failed", + "type": "counter", + "name": "UnwindNativeErrLookupStackDeltaInnerMap", + "field": "bpf.native.errors.lookup_stack_delta_inner_map", + "id": 70 + }, + { + "description": "Number of times that a lookup of the outer map for stack deltas failed", + "type": "counter", + "name": "UnwindNativeErrLookupStackDeltaOuterMap", + "field": "bpf.native.errors.lookup_stack_delta_outer_map", + "id": 71 + }, + { + "obsolete": true, + "description": "Number of times a PID maps file cannot be read as /proc/ folder does not exist anymore", + "type": "counter", + "name": "ErrProcPIDRead", + "id": 72 + }, + { + "obsolete": true, + "description": "Number of times that the deletion of entries from hash_to_framelist failed", + "type": "counter", + "name": "ErrHashToFrameListDelete", + "field": "agent.errors.hash_to_frame_list_delete", + "id": 73 + }, + { + "obsolete": true, + "description": "Number of times that the lookup of entries from hash_to_framelist failed", + "type": "counter", + "name": "ErrHashToFrameListLookup", + "field": "agent.errors.hash_to_frame_list_lookup", + "id": 74 + }, + { + "description": "Number of times the bpf helper failed to get the current comm of the task", + "type": "counter", + "name": "ErrBPFCurrentComm", + "field": "bpf.errors.bpf_current_comm", + "id": 75 + }, + { + "description": "Number of attempted PHP unwinds", + "type": "counter", + "name": "UnwindPHPAttempts", + "field": "bpf.php.attempts", + "id": 76 + }, + { + "description": "Number of unwound PHP frames", + "type": "counter", + "name": "UnwindPHPFrames", + "field": "bpf.php.frames", + "id": 77 + }, + { + "description": "Number of failures to read PHP current execute data pointer", + "type": "counter", + "name": "UnwindPHPErrBadCurrentExecuteData", + "field": "bpf.php.errors.bad_current_execute_data", + "id": 78 + }, + { + "description": "Number of failures to read PHP execute data contents", + "type": "counter", + "name": "UnwindPHPErrBadZendExecuteData", + "field": "bpf.php.errors.bad_zend_execute_data", + "id": 79 + }, + { + "description": "Number of failures to read PHP zend function contents", + "type": "counter", + "name": "UnwindPHPErrBadZendFunction", + "field": "bpf.php.errors.bad_zend_function", + "id": 80 + }, + { + "description": "Number of failures to read PHP zend opline contents", + "type": "counter", + "name": "UnwindPHPErrBadZendOpline", + "field": "bpf.php.errors.bad_zend_opline", + "id": 81 + }, + { + "description": "Number of LRU hits for kernel symbols", + "type": "counter", + "name": "KernelFallbackSymbolLRUHit", + "field": "agent.kernel.fallback_symbol_lru.hits", + "id": 82 + }, + { + "description": "Number of LRU mises for kernel symbols", + "type": "counter", + "name": "KernelFallbackSymbolLRUMiss", + "field": "agent.kernel.fallback_symbol_lru.misses", + "id": 83 + }, + { + "description": "Number of cache hits for ELF information", + "type": "counter", + "name": "ELFInfoCacheHit", + "field": "agent.elf_info_cache.hits", + "id": 84 + }, + { + "description": "Number of cache misses for ELF information", + "type": "counter", + "name": "ELFInfoCacheMiss", + "field": "agent.elf_info_cache.misses", + "id": 85 + }, + { + "description": "Number of successfully symbolized PHP frames", + "type": "counter", + "name": "PHPSymbolizationSuccess", + "field": "agent.php.symbolization.successes", + "id": 86 + }, + { + "description": "Number of PHP frames that failed symbolization", + "type": "counter", + "name": "PHPSymbolizationFailure", + "field": "agent.php.symbolization.failures", + "id": 87 + }, + { + "description": "Number of cache hits for Python AddrToCodeObject", + "type": "counter", + "name": "PythonAddrToCodeObjectHit", + "field": "agent.python.addr_to_code_object.hits", + "id": 88 + }, + { + "description": "Number of cache misses for Python AddrToCodeObject", + "type": "counter", + "name": "PythonAddrToCodeObjectMiss", + "field": "agent.python.addr_to_code_object.misses", + "id": 89 + }, + { + "description": "Number of cache hits for Hotspot AddrToSymbol", + "type": "counter", + "name": "HotspotAddrToSymbolHit", + "field": "agent.hotspot.addr_to_symbol.hits", + "id": 90 + }, + { + "description": "Number of cache misses for Hotspot AddrToSymbol", + "type": "counter", + "name": "HotspotAddrToSymbolMiss", + "field": "agent.hotspot.addr_to_symbol.misses", + "id": 91 + }, + { + "description": "Number of cache hits for Hotspot AddrToMethod", + "type": "counter", + "name": "HotspotAddrToMethodHit", + "field": "agent.hotspot.addr_to_method.hits", + "id": 92 + }, + { + "description": "Number of cache misses for Hotspot AddrToMethod", + "type": "counter", + "name": "HotspotAddrToMethodMiss", + "field": "agent.hotspot.addr_to_method.misses", + "id": 93 + }, + { + "description": "Number of cache hits for Hotspot AddrToJITInfo", + "type": "counter", + "name": "HotspotAddrToJITInfoHit", + "field": "agent.hotspot.addr_to_jit_info.hits", + "id": 94 + }, + { + "description": "Number of cache misses for Hotspot AddrToJITInfo", + "type": "counter", + "name": "HotspotAddrToJITInfoMiss", + "field": "agent.hotspot.addr_to_jit_info.misses", + "id": 95 + }, + { + "description": "Number of cache hits for PHP AddrToFunc", + "type": "counter", + "name": "PHPAddrToFuncHit", + "field": "agent.php.addr_to_func.hits", + "id": 96 + }, + { + "description": "Number of cache misses for PHP AddrToFunc", + "type": "counter", + "name": "PHPAddrToFuncMiss", + "field": "agent.php.addr_to_func.misses", + "id": 97 + }, + { + "description": "Current size in bytes of the local interval cache", + "type": "gauge", + "name": "LocalIntervalCacheSize", + "field": "agent.local_interval_cache.size", + "unit": "byte", + "id": 98 + }, + { + "description": "Number of cache hits of the local interval cache", + "type": "counter", + "name": "LocalIntervalCacheHit", + "field": "agent.local_interval_cache.hits", + "id": 99 + }, + { + "description": "Number of cache misses of the local interval cache", + "type": "counter", + "name": "LocalIntervalCacheMiss", + "field": "agent.local_interval_cache.misses", + "id": 100 + }, + { + "description": "Number of times a perf event was received without data (report_events)", + "type": "counter", + "name": "PerfEventNoData", + "field": "agent.errors.perf_event_no_data", + "id": 101 + }, + { + "description": "Number of times a perf event read failed (report_events)", + "type": "counter", + "name": "PerfEventReadError", + "field": "agent.errors.perf_event_read_error", + "id": 102 + }, + { + "obsolete": true, + "description": "Indicates if probabilistic sampling is en- or disabled. 1 sampling is enabled - see ebpf.probSampleEnable. -1 sampling is disabled - see ebpf.probSampleDisable.", + "type": "gauge", + "name": "ProbSampleStatus", + "id": 103 + }, + { + "obsolete": true, + "description": "Number of times the cache for pre-to-post conversion trace hashes was hit.", + "type": "counter", + "name": "HashMapperCacheHit", + "id": 104 + }, + { + "obsolete": true, + "description": "Number of times the cache for pre-to-post conversion trace hashes was missed.", + "type": "counter", + "name": "HashMapperCacheMiss", + "id": 105 + }, + { + "description": "Number of successfully symbolized Ruby frames", + "type": "counter", + "name": "RubySymbolizationSuccess", + "field": "agent.ruby.symbolization.successes", + "id": 106 + }, + { + "description": "Number of Ruby frames that failed symbolization", + "type": "counter", + "name": "RubySymbolizationFailure", + "field": "agent.ruby.symbolization.failures", + "id": 107 + }, + { + "description": "Number of attempted Ruby unwinds", + "type": "counter", + "name": "UnwindRubyAttempts", + "field": "bpf.ruby.attempts", + "id": 108 + }, + { + "description": "Number of unwound Ruby frames", + "type": "counter", + "name": "UnwindRubyFrames", + "field": "bpf.ruby.frames", + "id": 109 + }, + { + "description": "Number of cache hits for Ruby IseqBodyPCToFunction", + "type": "counter", + "name": "RubyIseqBodyPCHit", + "field": "agent.ruby.iseq_body_pc.hits", + "id": 110 + }, + { + "description": "Number of cache misses for Ruby IseqBodyPCToFunction", + "type": "counter", + "name": "RubyIseqBodyPCMiss", + "field": "agent.ruby.iseq_body_pc.misses", + "id": 111 + }, + { + "description": "Number of cache hits for Ruby AddrToString", + "type": "counter", + "name": "RubyAddrToStringHit", + "field": "agent.ruby.addr_to_string.hits", + "id": 112 + }, + { + "description": "Number of cache misses for Ruby AddrToString", + "type": "counter", + "name": "RubyAddrToStringMiss", + "field": "agent.ruby.addr_to_string.misses", + "id": 113 + }, + { + "obsolete": true, + "description": "Number of times a pre-conversion hash has not been present in the traceHashMapper used by the traceHandler", + "type": "counter", + "name": "TraceHashMapperMissingEntry", + "id": 114 + }, + { + "description": "Number of attempted perl unwinds", + "type": "counter", + "name": "UnwindPerlAttempts", + "field": "bpf.perl.attempts", + "id": 115 + }, + { + "description": "Number of unwound perl frames", + "type": "counter", + "name": "UnwindPerlFrames", + "field": "bpf.perl.frames", + "id": 116 + }, + { + "description": "Number of failures to read perl TLS info", + "type": "counter", + "name": "UnwindPerlTLS", + "field": "bpf.perl.errors.tls", + "id": 117 + }, + { + "description": "Number of failures to read perl stack info", + "type": "counter", + "name": "UnwindPerlReadStackInfo", + "field": "bpf.perl.errors.read_stack_info", + "id": 118 + }, + { + "description": "Number of failures to read perl context stack entry", + "type": "counter", + "name": "UnwindPerlReadContextStackEntry", + "field": "bpf.perl.errors.read_context_stack_entry", + "id": 119 + }, + { + "description": "Number of failures to resolve perl EGV", + "type": "counter", + "name": "UnwindPerlResolveEGV", + "field": "bpf.perl.errors.resolve_egv", + "id": 120 + }, + { + "description": "Number of successfully symbolized Perl frames", + "type": "counter", + "name": "PerlSymbolizationSuccess", + "field": "agent.perl.symbolization.successes", + "id": 121 + }, + { + "description": "Number of Perl frames that failed symbolization", + "type": "counter", + "name": "PerlSymbolizationFailure", + "field": "agent.perl.symbolization.failures", + "id": 122 + }, + { + "description": "Number of cache hits for Perl AddrToHEK", + "type": "counter", + "name": "PerlAddrToHEKHit", + "field": "agent.perl.addr_to_hek.hits", + "id": 123 + }, + { + "description": "Number of cache misses for Perl AddrToHEK", + "type": "counter", + "name": "PerlAddrToHEKMiss", + "field": "agent.perl.addr_to_hek.misses", + "id": 124 + }, + { + "description": "Number of cache hits for Perl AddrToCOP", + "type": "counter", + "name": "PerlAddrToCOPHit", + "field": "agent.perl.addr_to_cop.hits", + "id": 125 + }, + { + "description": "Number of cache misses for Perl AddrToCOP", + "type": "counter", + "name": "PerlAddrToCOPMiss", + "field": "agent.perl.addr_to_cop.misses", + "id": 126 + }, + { + "description": "Number of cache hits for Perl AddrToGV", + "type": "counter", + "name": "PerlAddrToGVHit", + "field": "agent.perl.addr_to_gv.hits", + "id": 127 + }, + { + "description": "Number of cache misses for Perl AddrToGV", + "type": "counter", + "name": "PerlAddrToGVMiss", + "field": "agent.perl.addr_to_gv.misses", + "id": 128 + }, + { + "obsolete": true, + "description": "Number of failures to unwind because PC is outside matched codeblob code range", + "type": "counter", + "name": "UnwindHotspotErrPCOutsideCodeblobCode", + "id": 129 + }, + { + "description": "Number of failures to unwind because return address was not found with heuristic", + "type": "counter", + "name": "UnwindHotspotErrInvalidRA", + "field": "bpf.hotspot.errors.invalid_ra", + "id": 130 + }, + { + "description": "Number of cache hits in tracehandler trace cache by BPF hash", + "type": "counter", + "name": "KnownTracesHit", + "field": "bpf.known_traces.hits", + "id": 131 + }, + { + "description": "Number of cache misses in tracehandler trace cache by BPF hash", + "type": "counter", + "name": "KnownTracesMiss", + "field": "bpf.known_traces.misses", + "id": 132 + }, + { + "description": "Current size of the unwind info array", + "type": "gauge", + "name": "UnwindInfoArraySize", + "field": "agent.unwind_info_array.size", + "id": 133 + }, + { + "description": "Current size of the stack delta pages hash map", + "type": "gauge", + "name": "HashmapNumStackDeltaPages", + "field": "agent.stack_delta_pages.size", + "id": 134 + }, + { + "obsolete": true, + "description": "Number of elements zapped from known traces", + "type": "counter", + "name": "KnownTracesZapCount", + "id": 135 + }, + { + "description": "Number of attempted V8 unwinds", + "type": "counter", + "name": "UnwindV8Attempts", + "field": "bpf.v8.attempts", + "id": 136 + }, + { + "description": "Number of unwound V8 frames", + "type": "counter", + "name": "UnwindV8Frames", + "field": "bpf.v8.frames", + "id": 137 + }, + { + "description": "Number of failures to read V8 frame pointer data", + "type": "counter", + "name": "UnwindV8ErrBadFP", + "field": "bpf.v8.errors.bad_fp", + "id": 138 + }, + { + "description": "Number of failures to read V8 Code/JSFunction object", + "type": "counter", + "name": "UnwindV8ErrBadJSFunc", + "field": "bpf.v8.errors.bad_js_func", + "id": 139 + }, + { + "description": "Number of failures to read V8 Code object", + "type": "counter", + "name": "UnwindV8ErrBadCode", + "field": "bpf.v8.errors.bad_code", + "id": 140 + }, + { + "description": "Number of successfully symbolized V8 frames", + "type": "counter", + "name": "V8SymbolizationSuccess", + "field": "agent.v8.symbolization.successes", + "id": 141 + }, + { + "description": "Number of V8 frames that failed symbolization", + "type": "counter", + "name": "V8SymbolizationFailure", + "field": "agent.v8.symbolization.failures", + "id": 142 + }, + { + "description": "Number of cache hits for V8 strings", + "type": "counter", + "name": "V8AddrToStringHit", + "field": "agent.v8.addr_to_string.hits", + "id": 143 + }, + { + "description": "Number of cache misses for V8 strings", + "type": "counter", + "name": "V8AddrToStringMiss", + "field": "agent.v8.addr_to_string.misses", + "id": 144 + }, + { + "description": "Number of cache hits for V8 SharedFunctionInfo", + "type": "counter", + "name": "V8AddrToSFIHit", + "field": "agent.v8.addr_to_sfi.hits", + "id": 145 + }, + { + "description": "Number of cache misses for V8 SharedFunctionInfo", + "type": "counter", + "name": "V8AddrToSFIMiss", + "field": "agent.v8.addr_to_sfi.misses", + "id": 146 + }, + { + "description": "Number of cache hits for V8 Code/JSFunction", + "type": "counter", + "name": "V8AddrToFuncHit", + "field": "agent.v8.addr_to_func.hits", + "id": 147 + }, + { + "description": "Number of cache misses for V8 Code/JSFunction", + "type": "counter", + "name": "V8AddrToFuncMiss", + "field": "agent.v8.addr_to_func.misses", + "id": 148 + }, + { + "description": "Number of cache hits for V8 Source", + "type": "counter", + "name": "V8AddrToSourceHit", + "field": "agent.v8.addr_to_source.hits", + "id": 149 + }, + { + "description": "Number of cache misses for V8 Source", + "type": "counter", + "name": "V8AddrToSourceMiss", + "field": "agent.v8.addr_to_source.misses", + "id": 150 + }, + { + "description": "Number of cache hits for Hotspot AddrToStubNameID", + "type": "counter", + "name": "HotspotAddrToStubNameIDHit", + "field": "agent.hotspot.addr_to_stub_name_id.hits", + "id": 151 + }, + { + "description": "Number of cache misses for Hotspot AddrToStubNameID", + "type": "counter", + "name": "HotspotAddrToStubNameIDMiss", + "field": "agent.hotspot.addr_to_stub_name_id.misses", + "id": 152 + }, + { + "description": "Outgoing total RPC byte count (payload, uncompressed)", + "type": "counter", + "name": "RPCBytesOutCount", + "field": "agent.rpc_bytes_out", + "unit": "byte", + "id": 153 + }, + { + "description": "Incoming total RPC byte count (payload, uncompressed)", + "type": "counter", + "name": "RPCBytesInCount", + "field": "agent.rpc_bytes_in", + "unit": "byte", + "id": 154 + }, + { + "description": "Number of times reading /proc/ failed due to missing text section", + "type": "counter", + "name": "ErrProcNoTextSec", + "field": "agent.errors.proc_no_text_section", + "id": 155 + }, + { + "description": "Number of times reading /proc/ as it does not exist anymore", + "type": "counter", + "name": "ErrProcNotExist", + "field": "agent.errors.proc_not_exists", + "id": 156 + }, + { + "description": "Number of times process exits while reading /proc/", + "type": "counter", + "name": "ErrProcESRCH", + "field": "agent.errors.proc_esrch", + "id": 157 + }, + { + "description": "Number of times reading /proc/ failed due to missing permission", + "type": "counter", + "name": "ErrProcPerm", + "field": "agent.errors.proc_perm", + "id": 158 + }, + { + "obsolete": true, + "description": "Number of deferred trace updates", + "type": "counter", + "name": "TraceNumDeferred", + "field": "agent.trace_num_deferred", + "id": 159 + }, + { + "obsolete": true, + "description": "Current size of the trace handler / hashmapperlru string intern table", + "type": "gauge", + "name": "HashMapperInternTableSize", + "field": "agent.hash_mapper_intern_table.size", + "id": 160 + }, + { + "description": "Number of added cache elements for Perl AddrToHEK", + "type": "counter", + "name": "PerlAddrToHEKAdd", + "field": "agent.perl.addr_to_hek.add", + "id": 161 + }, + { + "description": "Number of deleted cache elements for Perl AddrToHEK", + "type": "counter", + "name": "PerlAddrToHEKDel", + "field": "agent.perl.addr_to_hek.del", + "id": 162 + }, + { + "description": "Number of added cache elements for Perl AddrToCOP", + "type": "counter", + "name": "PerlAddrToCOPAdd", + "field": "agent.perl.addr_to_cop.add", + "id": 163 + }, + { + "description": "Number of deleted cache elements for Perl AddrToCOP", + "type": "counter", + "name": "PerlAddrToCOPDel", + "field": "agent.perl.addr_to_cop.del", + "id": 164 + }, + { + "description": "Number of added cache elements for Perl AddrToGV", + "type": "counter", + "name": "PerlAddrToGVAdd", + "field": "agent.perl.addr_to_gv.add", + "id": 165 + }, + { + "description": "Number of deleted cache elementes Perl AddrToGV", + "type": "counter", + "name": "PerlAddrToGVDel", + "field": "agent.perl.addr_to_gv.del", + "id": 166 + }, + { + "description": "Number of added cache elements for Hotspot AddrToSymbol", + "type": "counter", + "name": "HotspotAddrToSymbolAdd", + "field": "agent.hotspot.addr_to_symbol.add", + "id": 167 + }, + { + "description": "Number of deleted cache elements for Hotspot AddrToSymbol", + "type": "counter", + "name": "HotspotAddrToSymbolDel", + "field": "agent.hotspot.addr_to_symbol.del", + "id": 168 + }, + { + "description": "Number of added cache elements for Hotspot AddrToMethod", + "type": "counter", + "name": "HotspotAddrToMethodAdd", + "field": "agent.hotspot.addr_to_method.add", + "id": 169 + }, + { + "description": "Number of deleted cache elements for Hotspot AddrToMethod", + "type": "counter", + "name": "HotspotAddrToMethodDel", + "field": "agent.hotspot.addr_to_method.del", + "id": 170 + }, + { + "description": "Number of added cache elements for Hotspot AddrToJITInfo", + "type": "counter", + "name": "HotspotAddrToJITInfoAdd", + "field": "agent.hotspot.addr_to_jit_info.add", + "id": 171 + }, + { + "description": "Number of deleted cache elements for Hotspot AddrToJITInfo", + "type": "counter", + "name": "HotspotAddrToJITInfoDel", + "field": "agent.hotspot.addr_to_jit_info.del", + "id": 172 + }, + { + "description": "Number of added cache elements for Hotspot AddrToStubNameID", + "type": "counter", + "name": "HotspotAddrToStubNameIDAdd", + "field": "agent.hotspot.addr_to_stub_name.add", + "id": 173 + }, + { + "description": "Number of deleted cache elements for Hotspot AddrToStubNameID", + "type": "counter", + "name": "HotspotAddrToStubNameIDDel", + "field": "agent.hotspot.addr_to_stub_name.del", + "id": 174 + }, + { + "description": "Number of added cache elements for PHP AddrToFunc", + "type": "counter", + "name": "PHPAddrToFuncAdd", + "field": "agent.php.addr_to_func.add", + "id": 175 + }, + { + "description": "Number of deleted cache elements for PHP AddrToFunc", + "type": "counter", + "name": "PHPAddrToFuncDel", + "field": "agent.php.addr_to_func.del", + "id": 176 + }, + { + "description": "Number of added cache elements for Python AddrToCodeObject", + "type": "counter", + "name": "PythonAddrToCodeObjectAdd", + "field": "agent.python.addr_to_code_object.add", + "id": 177 + }, + { + "description": "Number of deleted cache elements for Python AddrToCodeObject", + "type": "counter", + "name": "PythonAddrToCodeObjectDel", + "field": "agent.python.addr_to_code_object.del", + "id": 178 + }, + { + "description": "Number of added cache elements for Ruby IseqBodyPCToFunction", + "type": "counter", + "name": "RubyIseqBodyPCAdd", + "field": "agent.ruby.iseq_body_pc.add", + "id": 179 + }, + { + "description": "Number of deleted cache elements for Ruby IseqBodyPCToFunction", + "type": "counter", + "name": "RubyIseqBodyPCDel", + "field": "agent.ruby.iseq_body_pc.del", + "id": 180 + }, + { + "description": "Number of added cache elements for Ruby AddrToString", + "type": "counter", + "name": "RubyAddrToStringAdd", + "field": "agent.ruby.addr_to_string.add", + "id": 181 + }, + { + "description": "Number of deleted cache elements for Ruby AddrToString", + "type": "counter", + "name": "RubyAddrToStringDel", + "field": "agent.ruby.addr_to_string.del", + "id": 182 + }, + { + "description": "Number of added cache elements for V8 strings", + "type": "counter", + "name": "V8AddrToStringAdd", + "field": "agent.v8.addr_to_string.add", + "id": 183 + }, + { + "description": "Number of deleted cache elements for V8 strings", + "type": "counter", + "name": "V8AddrToStringDel", + "field": "agent.v8.addr_to_string.del", + "id": 184 + }, + { + "description": "Number of added cache elements for V8 SharedFunctionInfo", + "type": "counter", + "name": "V8AddrToSFIAdd", + "field": "agent.v8.addr_to_sfi.add", + "id": 185 + }, + { + "description": "Number of deleted cache elements for V8 SharedFunctionInfo", + "type": "counter", + "name": "V8AddrToSFIDel", + "field": "agent.v8.addr_to_sfi.del", + "id": 186 + }, + { + "description": "Number of added cache elements for V8 Code/JSFunction", + "type": "counter", + "name": "V8AddrToFuncAdd", + "field": "agent.v8.addr_to_func.add", + "id": 187 + }, + { + "description": "Number of deleted cache elements for V8 Code/JSFunction", + "type": "counter", + "name": "V8AddrToFuncDel", + "field": "agent.v8.addr_to_func.del", + "id": 188 + }, + { + "description": "Number of added cache elements for V8 Source", + "type": "counter", + "name": "V8AddrToSourceAdd", + "field": "agent.v8.addr_to_source.add", + "id": 189 + }, + { + "description": "Number of deleted cache elements for V8 Source", + "type": "counter", + "name": "V8AddrToSourceDel", + "field": "agent.v8.addr_to_source.del", + "id": 190 + }, + { + "description": "Number of times we failed to update reported_pids", + "type": "counter", + "name": "ReportedPIDsErr", + "field": "bpf.errors.reported_pids", + "id": 191 + }, + { + "description": "Maximum number of size that was requested within the last reporting interval", + "type": "counter", + "name": "RubyMaxSize", + "field": "agent.ruby.max_size", + "id": 192 + }, + { + "description": "Maximum number of hekLen that was requested within the last reporting interval", + "type": "counter", + "name": "PerlHekLen", + "field": "agent.perl.hek_len", + "id": 193 + }, + { + "description": "Number of times frame unwinding failed because of LR == 0", + "type": "counter", + "name": "UnwindNativeLr0", + "field": "bpf.native.errors.lr0", + "id": 194 + }, + { + "description": "Number of times updating an element in unwindInfoArray failed", + "type": "counter", + "name": "UnwindInfoArrayUpdate", + "field": "agent.errors.unwind_info_array_update", + "id": 195 + }, + { + "description": "Number of times updating an element in exeIDToStackDeltas failed", + "type": "counter", + "name": "ExeIDToStackDeltasUpdate", + "field": "agent.errors.exe_id_to_stack_deltas_update", + "id": 196 + }, + { + "description": "Number of times deleting an element from exeIDToStackDeltas failed", + "type": "counter", + "name": "ExeIDToStackDeltasDelete", + "field": "agent.errors.exe_id_to_stack_deltas_delete", + "id": 197 + }, + { + "description": "Number of times updating an element in stackDeltaPageToInfo failed", + "type": "counter", + "name": "StackDeltaPageToInfoUpdate", + "field": "agent.errors.stack_delta_page_to_info_update", + "id": 198 + }, + { + "description": "Number of times deleting an element from stackDeltaPageToInfo failed", + "type": "counter", + "name": "StackDeltaPageToInfoDelete", + "field": "agent.errors.stack_delta_page_to_info_delete", + "id": 199 + }, + { + "description": "Number of times updating an element in pidPageToMappingInfo failed", + "type": "counter", + "name": "PidPageToMappingInfoUpdate", + "field": "agent.errors.pid_page_to_mapping_info_update", + "id": 200 + }, + { + "description": "Number of times deleting an element from pidPageToMappingInfo failed", + "type": "counter", + "name": "PidPageToMappingInfoDelete", + "field": "agent.errors.pid_page_to_mapping_info_delete", + "id": 201 + }, + { + "description": "Number of cache hits in the stack delta provider", + "type": "counter", + "name": "StackDeltaProviderCacheHit", + "field": "agent.stack_delta_provider_cache.hits", + "id": 202 + }, + { + "description": "Number of cache misses in the stack delta provider", + "type": "counter", + "name": "StackDeltaProviderCacheMiss", + "field": "agent.stack_delta_provider_cache.misses", + "id": 203 + }, + { + "description": "Number of times the stack delta provider failed to extract stack deltas", + "type": "counter", + "name": "StackDeltaProviderExtractionError", + "field": "agent.errors.stack_delta_provider_extraction", + "id": 204 + }, + { + "description": "Number of cache hits in tracehandler trace cache by UM hash", + "type": "counter", + "name": "TraceCacheHit", + "field": "agent.trace_cache.hits", + "id": 205 + }, + { + "description": "Number of cache misses in tracehandler trace cache by UM hash", + "type": "counter", + "name": "TraceCacheMiss", + "field": "agent.trace_cache.misses", + "id": 206 + }, + { + "description": "Number of /proc/PID/maps process attempts", + "type": "counter", + "name": "NumProcAttempts", + "field": "agent.num_proc_attempts", + "id": 207 + }, + { + "description": "Number of times finding the return address in the interpreter loop failed for PHP 8+.", + "type": "counter", + "name": "PHPFailedToFindReturnAddress", + "field": "agent.php.errors.failed_to_find_return_address", + "id": 208 + }, + { + "description": "Number of times we encountered frame sizes larger than the supported maximum", + "type": "counter", + "name": "HotspotUnsupportedFrameSize", + "field": "bpf.hotspot.errors.unsupported_frame_size", + "id": 209 + }, + { + "obsolete": true, + "description": "Number of lost perf events in the communication between kernel and user space (report_munmap_events)", + "type": "counter", + "name": "PerfEventLostMunmap", + "id": 210 + }, + { + "obsolete": true, + "description": "Number of times a perf event was received without data (report_munmap_events)", + "type": "counter", + "name": "PerfEventNoDataMunmap", + "id": 211 + }, + { + "obsolete": true, + "description": "Number of times a perf event read failed (report_munmap_events)", + "type": "counter", + "name": "PerfEventReadErrorMunmap", + "id": 212 + }, + { + "description": "Number of new PID events (report_events)", + "type": "counter", + "name": "NumProcNew", + "field": "bpf.num_proc_new", + "id": 213 + }, + { + "description": "Number of exit PID events (report_events)", + "type": "counter", + "name": "NumProcExit", + "field": "bpf.num_proc_exit", + "id": 214 + }, + { + "description": "Number of unknown PC events (report_events)", + "type": "counter", + "name": "NumUnknownPC", + "field": "bpf.num_unknown_pc", + "id": 215 + }, + { + "obsolete": true, + "description": "Number of symbolize trace events (report_events)", + "type": "counter", + "name": "NumSymbolizeTrace", + "field": "bpf.num_symbolize_trace", + "id": 216 + }, + { + "obsolete": true, + "description": "Number of munmap events (report_munmap_events)", + "type": "counter", + "name": "NumMunmapEvent", + "id": 217 + }, + { + "description": "Max /proc/PID/maps parse time for a single collection interval, in microseconds", + "type": "counter", + "name": "MaxProcParseUsec", + "field": "agent.max_proc_parse.us", + "unit": "micros", + "id": 218 + }, + { + "description": "Time spent processing /proc/PID/maps on startup, in milliseconds", + "type": "counter", + "name": "ProcPIDStartupMs", + "field": "agent.time.proc_pid_startup", + "unit": "ms", + "id": 219 + }, + { + "description": "Total /proc/PID/maps parse time for a single collection interval, in microseconds", + "type": "counter", + "name": "TotalProcParseUsec", + "field": "agent.time.total_proc_parse", + "unit": "micros", + "id": 220 + }, + { + "description": "Number of kubernetes client queries.", + "type": "counter", + "name": "KubernetesClientQuery", + "field": "agent.kubernetes_client_query", + "id": 221 + }, + { + "description": "Number of docker client queries.", + "type": "counter", + "name": "DockerClientQuery", + "field": "agent.docker_client_query", + "id": 222 + }, + { + "description": "Number of containerd client queries.", + "type": "counter", + "name": "ContainerdClientQuery", + "field": "agent.containerd_client_query", + "id": 223 + }, + { + "obsolete": true, + "description": "Time spent between parsing the eBPF maps to collect traces and counts, in milliseconds", + "type": "counter", + "name": "MonitorHashMapsIntervalMs", + "unit": "ms", + "id": 224 + }, + { + "obsolete": true, + "description": "Number of eBPF trace/count collection operations", + "type": "counter", + "name": "NumMonitorHashMaps", + "field": "agent.num_monitor_hash_maps", + "id": 225 + }, + { + "description": "Number of generic PID events (report_events)", + "type": "counter", + "name": "NumGenericPID", + "field": "agent.num_generic_pid", + "id": 226 + }, + { + "description": "Number of times we failed to update pid_events", + "type": "counter", + "name": "PIDEventsErr", + "field": "bpf.errors.pid_events", + "id": 227 + }, + { + "description": "Number of failures to read _PyCFrame.current_frame in unwind_python()", + "type": "counter", + "name": "UnwindPythonErrBadCFrameFrameAddr", + "field": "bpf.python.errors.bad_cframe_frame_addr", + "id": 228 + }, + { + "description": "Number of times stack unwinding was stopped to not hit the limit of tail calls", + "type": "counter", + "name": "MaxTailCalls", + "field": "bpf.tail_calls_max", + "id": 229 + }, + { + "description": "Indicates if probabilistic profiling is enabled or disabled: 1 profiling is enabled, -1 profiling is disabled.", + "type": "gauge", + "name": "ProbProfilingStatus", + "id": 230 + }, + { + "description": "Interval in seconds for which probabilistic profiling will be enabled or disabled.", + "type": "counter", + "name": "ProbProfilingInterval", + "id": 231, + "unit": "s" + }, + { + "description": "Number of times enabling a perf event hook failed", + "type": "counter", + "name": "PerfEventEnableErr", + "id": 232 + }, + { + "description": "Number of times disabling a perf event hook failed", + "type": "counter", + "name": "PerfEventDisableErr", + "id": 233 + }, + { + "description": "Number of times we didn't find an entry for this process in the Python process info array", + "type": "counter", + "name": "UnwindPythonErrNoProcInfo", + "field": "bpf.python.errors.no_proc_info", + "id": 234 + }, + { + "description": "Number of failures to read autoTLSkey", + "type": "counter", + "name": "UnwindPythonErrBadAutoTlsKeyAddr", + "field": "bpf.python.errors.bad_auto_tls_key_addr", + "id": 235 + }, + { + "description": "Number of failures to read the thread state pointer from TLD", + "type": "counter", + "name": "UnwindPythonErrReadThreadStateAddr", + "field": "bpf.python.errors.read_thread_state_addr", + "id": 236 + }, + { + "description": "Number of failures to determine the base address for thread-specific data", + "type": "counter", + "name": "UnwindPythonErrReadTsdBase", + "field": "bpf.python.errors.read_tsd_base", + "id": 237 + }, + { + "description": "Number of times we didn't find an entry for this process in the Ruby process info array", + "type": "counter", + "name": "UnwindRubyErrNoProcInfo", + "field": "bpf.ruby.errors.no_proc_info", + "id": 238 + }, + { + "description": "Number of failures to read the stack pointer from the Ruby context", + "type": "counter", + "name": "UnwindRubyErrReadStackPtr", + "field": "bpf.ruby.errors.read_stack_ptr", + "id": 239 + }, + { + "description": "Number of failures to read the size of the VM stack from the Ruby context", + "type": "counter", + "name": "UnwindRubyErrReadStackSize", + "field": "bpf.ruby.errors.read_stack_size", + "id": 240 + }, + { + "description": "Number of failures to read the control frame pointer from the Ruby context", + "type": "counter", + "name": "UnwindRubyErrReadCfp", + "field": "bpf.ruby.errors.read_cfp", + "id": 241 + }, + { + "description": "Number of failures to read the expression path from the Ruby frame", + "type": "counter", + "name": "UnwindRubyErrReadEp", + "field": "bpf.ruby.errors.read_ep", + "id": 242 + }, + { + "description": "Number of failures to read the instruction sequence body", + "type": "counter", + "name": "UnwindRubyErrReadIseqBody", + "field": "bpf.ruby.errors.read_iseq_body", + "id": 243 + }, + { + "description": "Number of failures to read the instruction sequence encoded size", + "type": "counter", + "name": "UnwindRubyErrReadIseqEncoded", + "field": "bpf.ruby.errors.read_iseq_encoded", + "id": 244 + }, + { + "description": "Number of failures to read the instruction sequence size", + "type": "counter", + "name": "UnwindRubyErrReadIseqSize", + "field": "bpf.ruby.errors.read_iseq_size", + "id": 245 + }, + { + "description": "Number of times the unwind instructions requested LR unwinding mid-trace", + "type": "counter", + "name": "UnwindNativeErrLrUnwindingMidTrace", + "field": "bpf.native.errors.lr_unwinding_mid_trace", + "id": 246 + }, + { + "description": "Number of failures to read the kernel-mode registers", + "type": "counter", + "name": "UnwindNativeErrReadKernelModeRegs", + "field": "bpf.native.errors.read_kernel_mode_regs", + "id": 247 + }, + { + "description": "Number of failures to read the IRQ stack link", + "type": "counter", + "name": "UnwindNativeErrChaseIrqStackLink", + "field": "bpf.native.errors.chase_irq_stack_link", + "id": 248 + }, + { + "description": "Number of times we didn't find an entry for this process in the V8 process info array", + "type": "counter", + "name": "UnwindV8ErrNoProcInfo", + "field": "bpf.v8.errors.no_proc_info", + "id": 249 + }, + { + "description": "Number of times an unwind_info_array index was invalid", + "type": "counter", + "name": "UnwindNativeErrBadUnwindInfoIndex", + "field": "bpf.native.errors.bad_unwind_info_index", + "id": 250 + }, + { + "description": "Number of times batch updating elements in exeIDToStackDeltas failed", + "type": "counter", + "name": "ExeIDToStackDeltasBatchUpdate", + "field": "agent.errors.exe_id_to_stack_deltas_batch_update", + "id": 251 + }, + { + "description": "Number of times batch updating elements in stackDeltaPageToInfo failed", + "type": "counter", + "name": "StackDeltaPageToInfoBatchUpdate", + "field": "agent.errors.stack_delta_page_to_info_batch_update", + "id": 252 + }, + { + "description": "Number of times batch deleting elements from pidPageToMappingInfo failed", + "type": "counter", + "name": "PidPageToMappingInfoBatchDelete", + "field": "agent.errors.pid_page_to_mapping_info_batch_delete", + "id": 253 + }, + { + "description": "Outgoing total RPC byte count (on-the-wire, compressed)", + "type": "counter", + "name": "WireBytesOutCount", + "field": "agent.wire_bytes_out", + "unit": "byte", + "id": 254 + }, + { + "description": "Incoming total RPC byte count (on-the-wire, compressed)", + "type": "counter", + "name": "WireBytesInCount", + "field": "agent.wire_bytes_in", + "unit": "byte", + "id": 255 + }, + { + "description": "Number of times the Hotspot unwind instructions requested LR unwinding mid-trace", + "type": "counter", + "name": "UnwindHotspotErrLrUnwindingMidTrace", + "field": "bpf.hotspot.errors.lr_unwinding_mid_trace", + "id": 256 + } +] diff --git a/metrics/reportermetrics/reportermetrics.go b/metrics/reportermetrics/reportermetrics.go new file mode 100644 index 00000000..6fa7d010 --- /dev/null +++ b/metrics/reportermetrics/reportermetrics.go @@ -0,0 +1,82 @@ +/* + * 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 reportermetrics implements the fetching and reporting of agent specific metrics. +package reportermetrics + +import ( + "context" + "time" + + "github.com/elastic/otel-profiling-agent/reporter" + + "github.com/elastic/otel-profiling-agent/libpf/periodiccaller" + "github.com/elastic/otel-profiling-agent/metrics" +) + +// report retrieves the reporter metrics and forwards these to the metrics package for processing. +func report(forReporter reporter.Reporter) { + reporterMetrics := forReporter.GetMetrics() + metrics.AddSlice([]metrics.Metric{ + { + ID: metrics.IDCountsForTracesOverwrite, + Value: metrics.MetricValue(reporterMetrics.CountsForTracesOverwriteCount), + }, + { + ID: metrics.IDExeMetadataOverwrite, + Value: metrics.MetricValue(reporterMetrics.ExeMetadataOverwriteCount), + }, + { + ID: metrics.IDFrameMetadataOverwrite, + Value: metrics.MetricValue(reporterMetrics.FrameMetadataOverwriteCount), + }, + { + ID: metrics.IDFramesForTracesOverwrite, + Value: metrics.MetricValue(reporterMetrics.FramesForTracesOverwriteCount), + }, + { + ID: metrics.IDHostMetadataOverwrite, + Value: metrics.MetricValue(reporterMetrics.HostMetadataOverwriteCount), + }, + { + ID: metrics.IDMetricsOverwrite, + Value: metrics.MetricValue(reporterMetrics.MetricsOverwriteCount), + }, + { + ID: metrics.IDFallbackSymbolsOverwrite, + Value: metrics.MetricValue(reporterMetrics.FallbackSymbolsOverwriteCount), + }, + { + ID: metrics.IDRPCBytesOutCount, + Value: metrics.MetricValue(reporterMetrics.RPCBytesOutCount), + }, + { + ID: metrics.IDRPCBytesInCount, + Value: metrics.MetricValue(reporterMetrics.RPCBytesInCount), + }, + { + ID: metrics.IDWireBytesOutCount, + Value: metrics.MetricValue(reporterMetrics.WireBytesOutCount), + }, + { + ID: metrics.IDWireBytesInCount, + Value: metrics.MetricValue(reporterMetrics.WireBytesInCount), + }, + }) +} + +// Start starts the reporter specific metric retrieval and reporting. +func Start(mainCtx context.Context, rep reporter.Reporter, interval time.Duration) func() { + ctx, cancel := context.WithCancel(mainCtx) + stopReporting := periodiccaller.Start(ctx, interval, func() { + report(rep) + }) + + return func() { + cancel() + stopReporting() + } +} diff --git a/metrics/types.go b/metrics/types.go new file mode 100644 index 00000000..88dc35ac --- /dev/null +++ b/metrics/types.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 metrics + +// Create ids.go from metrics.json +//go:generate go run genids/main.go metrics.json ids.go + +// MetricID is the type for metric IDs. +type MetricID uint16 + +// MetricValue is the type for metric values. +type MetricValue int64 + +// Metric is the type for a metric id/value pair. +type Metric struct { + ID MetricID + Value MetricValue +} + +// Summary helps summarizing metrics of the same ID from different sources before +// processing it further. +type Summary map[MetricID]MetricValue diff --git a/pacmask/pacmask_arm64.go b/pacmask/pacmask_arm64.go new file mode 100644 index 00000000..f2e8e5c4 --- /dev/null +++ b/pacmask/pacmask_arm64.go @@ -0,0 +1,71 @@ +//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. + * See the file "LICENSE" for details. + */ + +package pacmask + +import ( + "math/rand" +) + +// PACIA is an "intrinsic" for the A64 `pacia` instruction. +// +// Given a pointer, a modifier and the secret key stored in a system register +// that isn't visible to EL0 (user-mode), it computes a tag that is inserted +// into the pointer. The bits within the pointer where the tag is stored are +// ignored during address translation. If the hardware doesn't support PAC, the +// `ptr` argument is returned untouched. `ptr` needs to be aligned to 8 bytes. +func PACIA(ptr, modifier uint64) uint64 + +// GetPACMask determines the mask of where the PAC tag is located in a code +// pointer on ARM64. On architectures != ARM64, this function returns `0`. +// +// The PAC mask varies depending on kernel configs like `CONFIG_ARM64_VA_BITS` +// and `CONFIG_ARM64_MTE`, so we have to determine it dynamically. +func GetPACMask() uint64 { + // The official [1] way to retrieve the PAC mask on Linux is using ptrace + // and the `PTRACE_GETREGSET` method. However, since a program cannot debug + // itself, we'd have to spawn a process, attach for debugging, read the + // 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 + // signed "pointer", we then remove the 32 bits of randomness from the + // bottom, leaving us with just the PAC tag bits set by the `pacia` + // instruction. Repeating this sufficiently often, always ANDing the result + // with the previous values, after a few iterations, we're statistically + // pretty much guaranteed to set all bits that belong to the mask. + // + // With 32 iterations, assuming 4 PAC bits and an even hash distribution in + // the PAC bits, the chance for this to work out fine should be: + // + // (1 - 0.5 ** 32) ** 4 = 0.9999999990686774 + // + // With 64 iterations, IEEE floats are no longer able to express the odds, + // 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++ { + // The stack pointer on aarch64 needs to be aligned to 8 bytes at all + // times. The `<< 3` ensures that this is always the case for our fake + // pointers that will temporarily be placed as a fake stack pointer. + // nolint:gosec + probe := uint64(rand.Uint32() << 3) + // nolint:gosec + modifier := rand.Uint64() + probeWithPAC := PACIA(probe, modifier) + mask |= probeWithPAC & ^uint64(0xFFFF_FFFF) + } + + return mask +} diff --git a/pacmask/pacmask_arm64.s b/pacmask/pacmask_arm64.s new file mode 100644 index 00000000..05ff6cf3 --- /dev/null +++ b/pacmask/pacmask_arm64.s @@ -0,0 +1,36 @@ +//go:build arm64 + +// func PACIA(ptr, modifier uint64) uint64; +// +// This particular implementation of this intrinsic uses the `paciasp` +// instruction rather than the actual `pacia` instruction, even if that makes +// the implementation more complex due to the required register shuffling. The +// reason here is that `paciasp` is encoded in a space that was previously a +// `nop`, meaning that it is backward compatible to devices without PAC support. +// This isn't the case for the more generic `pacia` instruction. +TEXT ·PACIA(SB),$0-16 + // Backup original LR and SP. + MOVD LR, R1 + MOVD RSP, R2 + + // Move `ptr` into LR + MOVD ptr+0(FP), LR + + // Move `modifier` into SP. + MOVD modifier+8(FP), R0 + MOVD R0, RSP + + // `PACIASP` instruction. Go assembler doesn't support it yet. + WORD $0xD503233F + + // Temporarily place PAC'ed LR into X0, since the stack ptr isn't restored, yet. + MOVD LR, R0 + + // Restore original SP and LR. + MOVD R2, RSP + MOVD R1, LR + + // Place the return value on stack. + MOVD R0, r1+16(FP) + + RET diff --git a/pacmask/pacmask_other.go b/pacmask/pacmask_other.go new file mode 100644 index 00000000..f7166331 --- /dev/null +++ b/pacmask/pacmask_other.go @@ -0,0 +1,14 @@ +//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. + * See the file "LICENSE" for details. + */ + +package pacmask + +// GetPACMask always returns 0 on this platform. +func GetPACMask() uint64 { + return 0 +} diff --git a/proc/proc.go b/proc/proc.go new file mode 100644 index 00000000..216ba5cb --- /dev/null +++ b/proc/proc.go @@ -0,0 +1,189 @@ +/* + * 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 proc provides functionality for retrieving kallsyms, modules and +// executable mappings via /proc. +package proc + +import ( + "bufio" + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/elastic/otel-profiling-agent/libpf/stringutil" + "golang.org/x/sys/unix" + + "github.com/elastic/otel-profiling-agent/libpf" + log "github.com/sirupsen/logrus" +) + +const defaultMountPoint = "/proc" + +// GetKallsyms returns SymbolMap for kernel symbols from /proc/kallsyms. +func GetKallsyms(kallsymsPath string) (*libpf.SymbolMap, error) { + var address uint64 + var symbol string + + symmap := libpf.SymbolMap{} + noSymbols := true + + file, err := os.Open(kallsymsPath) + if err != nil { + return nil, fmt.Errorf("unable to open %s: %v", kallsymsPath, err) + } + defer file.Close() + + var scanner = bufio.NewScanner(file) + for scanner.Scan() { + // Avoid heap allocation by not using scanner.Text(). + // NOTE: The underlying bytes will change with the next call to scanner.Scan(), + // so make sure to not keep any references after the end of the loop iteration. + line := stringutil.ByteSlice2String(scanner.Bytes()) + + // Avoid heap allocations here - do not use strings.FieldsN() + var fields [4]string + nFields := stringutil.FieldsN(line, fields[:]) + + if nFields < 3 { + return nil, fmt.Errorf("unexpected line in kallsyms: '%s'", line) + } + + if address, err = strconv.ParseUint(fields[0], 16, 64); err != nil { + return nil, fmt.Errorf("failed to parse address value: '%s'", fields[0]) + } + + if address != 0 { + noSymbols = false + } + + symbol = strings.Clone(fields[2]) + + symmap.Add(libpf.Symbol{ + Name: libpf.SymbolName(symbol), + Address: libpf.SymbolValue(address), + }) + } + symmap.Finalize() + + if noSymbols { + return nil, fmt.Errorf( + "all addresses from kallsyms are zero - check process permissions") + } + + return &symmap, nil +} + +// GetKernelModules returns SymbolMap for kernel modules from /proc/modules. +func GetKernelModules(modulesPath string, + kernelSymbols *libpf.SymbolMap) (*libpf.SymbolMap, error) { + symmap := libpf.SymbolMap{} + + file, err := os.Open(modulesPath) + if err != nil { + return nil, fmt.Errorf("unable to open %s: %v", modulesPath, err) + } + defer file.Close() + + stext, err := kernelSymbols.LookupSymbol("_stext") + if err != nil { + return nil, fmt.Errorf("unable to find kernel text section start: %v", err) + } + etext, err := kernelSymbols.LookupSymbol("_etext") + if err != nil { + return nil, fmt.Errorf("unable to find kernel text section end: %v", err) + } + log.Debugf("Found KERNEL TEXT at %x-%x", stext.Address, etext.Address) + symmap.Add(libpf.Symbol{ + Name: "vmlinux", + Address: stext.Address, + Size: int(etext.Address - stext.Address), + }) + + var scanner = bufio.NewScanner(file) + for scanner.Scan() { + var size, refcount, address uint64 + var name, dependencies, state string + + line := scanner.Text() + + nFields, _ := fmt.Sscanf(line, "%s %d %d %s %s 0x%x", + &name, &size, &refcount, &dependencies, &state, &address) + if nFields < 6 { + return nil, fmt.Errorf("unexpected line in modules: '%s'", line) + } + if address == 0 { + return nil, fmt.Errorf( + "addresses from modules is zero - "+ + "check process permissions: '%s'", line) + } + + symmap.Add(libpf.Symbol{ + Name: libpf.SymbolName(name), + Address: libpf.SymbolValue(address), + Size: int(size), + }) + } + symmap.Finalize() + + 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) + files, err := os.ReadDir(defaultMountPoint) + if err != nil { + return nil, err + } + for _, f := range files { + // Make sure this is a PID file entry + if !f.IsDir() { + continue + } + pid, err := strconv.ParseUint(f.Name(), 10, 32) + if err != nil { + continue + } + pids = append(pids, libpf.PID(pid)) + } + return pids, nil +} + +// IsPIDLive checks if a PID belongs to a live process. It will never produce a false negative but +// may produce a false positive (e.g. due to permissions) in which case an error will also be +// returned. +func IsPIDLive(pid libpf.PID) (bool, error) { + // A kill syscall with a 0 signal is documented to still do the check + // whether the process exists: https://linux.die.net/man/2/kill + err := unix.Kill(int(pid), 0) + if err == nil { + return true, nil + } + + var errno unix.Errno + if errors.As(err, &errno) { + switch errno { + case unix.ESRCH: + return false, nil + case unix.EPERM: + // continue with procfs fallback + default: + return true, err + } + } + + path := fmt.Sprintf("%s/%d/maps", defaultMountPoint, pid) + _, err = os.Stat(path) + + if err != nil && os.IsNotExist(err) { + return false, nil + } + + return true, err +} diff --git a/proc/proc_test.go b/proc/proc_test.go new file mode 100644 index 00000000..f222085a --- /dev/null +++ b/proc/proc_test.go @@ -0,0 +1,48 @@ +/* + * 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 proc + +import ( + "testing" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +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) + } +} + +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") + } + + // Check parsing invalid file + symmap, err = GetKallsyms("testdata/kallsyms_invalid") + if symmap != nil || err == nil { + t.Fatalf("expected an error because file is invalid") + } + + // Happy case + symmap, err = GetKallsyms("testdata/kallsyms") + if err != nil { + t.Fatalf("error parsing kallsyms: %v", err) + } + + assertSymbol(t, symmap, "cpu_tss_rw", 0x6000) + assertSymbol(t, symmap, "hid_add_device", 0xffffffffc033e550) +} diff --git a/proc/testdata/kallsyms b/proc/testdata/kallsyms new file mode 100644 index 00000000..6b6f6736 --- /dev/null +++ b/proc/testdata/kallsyms @@ -0,0 +1,13 @@ +0000000000000000 A fixed_percpu_data +0000000000000000 A __per_cpu_start +0000000000001000 A cpu_debug_store +0000000000002000 A irq_stack_backing_store +0000000000006000 A cpu_tss_rw +ffffffffc0346f40 t hidraw_connect [hid] +ffffffffc0340470 t hidinput_find_field [hid] +ffffffffc033d150 t hid_parse_report [hid] +ffffffffc033f9a0 t hid_open_report [hid] +ffffffffc03459b0 t hid_quirks_init [hid] +ffffffffc0345720 t hid_ignore [hid] +ffffffffc033e550 t hid_add_device [hid] +ffffffffc0346e20 t hidraw_report_event [hid] diff --git a/proc/testdata/kallsyms_0 b/proc/testdata/kallsyms_0 new file mode 100644 index 00000000..c4e26e86 --- /dev/null +++ b/proc/testdata/kallsyms_0 @@ -0,0 +1,5 @@ +0000000000000000 A fixed_percpu_data +0000000000000000 A __per_cpu_start +0000000000000000 A cpu_debug_store +0000000000000000 A irq_stack_backing_store +0000000000000000 A cpu_tss_rw diff --git a/proc/testdata/kallsyms_invalid b/proc/testdata/kallsyms_invalid new file mode 100644 index 00000000..1b9c31a7 --- /dev/null +++ b/proc/testdata/kallsyms_invalid @@ -0,0 +1,5 @@ +0000000000000000 A fixed_percpu_data +0000000000000000 A __per_cpu_start +0000000000001000 A cpu_debug_store +0000000000002000 irq_stack_backing_store +0000000000006000 A cpu_tss_rw diff --git a/processmanager/ebpf/ebpf.go b/processmanager/ebpf/ebpf.go new file mode 100644 index 00000000..13daf5d7 --- /dev/null +++ b/processmanager/ebpf/ebpf.go @@ -0,0 +1,802 @@ +/* + * 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 ( + "errors" + "fmt" + "math/bits" + "sync" + "unsafe" + + cebpf "github.com/cilium/ebpf" + "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" + "github.com/elastic/otel-profiling-agent/support" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +/* +#include +#include "../../support/ebpf/types.h" +*/ +import "C" + +// EbpfHandler provides the functionality to interact with eBPF maps. +// nolint:revive +type EbpfHandler interface { + // Embed interpreter.EbpfHandler as subset of this interface. + interpreter.EbpfHandler + + // RemoveReportedPID removes a PID from the reported_pids eBPF map. + RemoveReportedPID(pid libpf.PID) + + // UpdateUnwindInfo writes UnwindInfo to given unwind info array index + UpdateUnwindInfo(index uint16, info sdtypes.UnwindInfo) error + + // UpdateExeIDToStackDeltas defines a function that updates the eBPF map exe_id_to_stack_deltas + // for host.FileID with the elements of StackDeltaEBPF. It returns the mapID used. + UpdateExeIDToStackDeltas(fileID host.FileID, deltas []StackDeltaEBPF) (uint16, error) + + // DeleteExeIDToStackDeltas defines a function that removes the entries from the outer eBPF + // map exe_id_to_stack_deltas and its associated inner map entries. + DeleteExeIDToStackDeltas(fileID host.FileID, mapID uint16) error + + // UpdateStackDeltaPages defines a function that updates the mapping in a eBPF map from + // a FileID and page to its stack delta lookup information. + UpdateStackDeltaPages(fileID host.FileID, numDeltasPerPage []uint16, + mapID uint16, firstPageAddr uint64) error + + // DeleteStackDeltaPage defines a function that removes the element specified by fileID and page + // from the eBPF map. + DeleteStackDeltaPage(fileID host.FileID, page uint64) error + + // 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 + + // 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) + + // CollectMetrics returns gathered errors for changes to eBPF maps. + CollectMetrics() []metrics.Metric + + // SupportsGenericBatchOperations returns true if the kernel supports eBPF batch operations + // on hash and array maps. + SupportsGenericBatchOperations() bool + + // SupportsLPMTrieBatchOperations returns true if the kernel supports eBPF batch operations + // on LPM trie maps. + SupportsLPMTrieBatchOperations() bool +} + +type ebpfMapsImpl struct { + // Interpreter related eBPF maps + interpreterOffsets *cebpf.Map + perlProcs *cebpf.Map + pyProcs *cebpf.Map + hotspotProcs *cebpf.Map + phpProcs *cebpf.Map + phpJITProcs *cebpf.Map + rubyProcs *cebpf.Map + v8Procs *cebpf.Map + + // Stackdelta and process related eBPF maps + exeIDToStackDeltaMaps []*cebpf.Map + stackDeltaPageToInfo *cebpf.Map + pidPageToMappingInfo *cebpf.Map + unwindInfoArray *cebpf.Map + reportedPIDs *cebpf.Map + + errCounterLock sync.Mutex + errCounter map[metrics.MetricID]int64 + + hasGenericBatchOperations bool + hasLPMTrieBatchOperations bool +} + +var outerMapsName = [...]string{ + "exe_id_to_8_stack_deltas", + "exe_id_to_9_stack_deltas", + "exe_id_to_10_stack_deltas", + "exe_id_to_11_stack_deltas", + "exe_id_to_12_stack_deltas", + "exe_id_to_13_stack_deltas", + "exe_id_to_14_stack_deltas", + "exe_id_to_15_stack_deltas", + "exe_id_to_16_stack_deltas", + "exe_id_to_17_stack_deltas", + "exe_id_to_18_stack_deltas", + "exe_id_to_19_stack_deltas", + "exe_id_to_20_stack_deltas", + "exe_id_to_21_stack_deltas", +} + +// Compile time check to make sure ebpfMapsImpl satisfies the interface . +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) { + impl := &ebpfMapsImpl{} + impl.errCounter = make(map[metrics.MetricID]int64) + + interpreterOffsets, ok := maps["interpreter_offsets"] + if !ok { + log.Fatalf("Map interpreter_offsets is not available") + } + impl.interpreterOffsets = interpreterOffsets + + perlProcs, ok := maps["perl_procs"] + if !ok { + log.Fatalf("Map perl_procs is not available") + } + impl.perlProcs = perlProcs + + pyProcs, ok := maps["py_procs"] + if !ok { + log.Fatalf("Map py_procs is not available") + } + impl.pyProcs = pyProcs + + hotspotProcs, ok := maps["hotspot_procs"] + if !ok { + log.Fatalf("Map hotspot_procs is not available") + } + impl.hotspotProcs = hotspotProcs + + phpProcs, ok := maps["php_procs"] + if !ok { + log.Fatalf("Map php_procs is not available") + } + 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") + } + impl.rubyProcs = rubyProcs + + v8Procs, ok := maps["v8_procs"] + if !ok { + log.Fatalf("Map v8_procs is not available") + } + impl.v8Procs = v8Procs + + impl.stackDeltaPageToInfo, ok = maps["stack_delta_page_to_info"] + if !ok { + log.Fatalf("Map stack_delta_page_to_info is not available") + } + + impl.pidPageToMappingInfo, ok = maps["pid_page_to_mapping_info"] + if !ok { + log.Fatalf("Map pid_page_to_mapping_info is not available") + } + + impl.unwindInfoArray, ok = maps["unwind_info_array"] + if !ok { + log.Fatalf("Map unwind_info_array is not available") + } + + impl.reportedPIDs, ok = maps["reported_pids"] + if !ok { + log.Fatalf("Map reported_pids is not available") + } + + impl.exeIDToStackDeltaMaps = make([]*cebpf.Map, len(outerMapsName)) + for i := support.StackDeltaBucketSmallest; i <= support.StackDeltaBucketLargest; i++ { + deltasMapName := fmt.Sprintf("exe_id_to_%d_stack_deltas", i) + deltasMap, ok := maps[deltasMapName] + if !ok { + log.Fatalf("Map %s is not available", deltasMapName) + } + impl.exeIDToStackDeltaMaps[i-support.StackDeltaBucketSmallest] = deltasMap + } + + if probeBatchOperations(cebpf.Hash) { + log.Infof("Supports generic eBPF map batch operations") + impl.hasGenericBatchOperations = true + } + + if probeBatchOperations(cebpf.LPMTrie) { + log.Infof("Supports LPM trie eBPF map batch operations") + impl.hasLPMTrieBatchOperations = true + } + + 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 { + if offsetRanges == nil { + return fmt.Errorf("offsetRanges is nil") + } + for _, offsetRange := range offsetRanges { + // The keys of this map are executable-id-and-offset-into-text entries, and + // the offset_range associated with them gives the precise area in that page + // where the main interpreter loop is located. This is required to unwind + // nicely from native code into interpreted code. + key := uint64(fileID) + value := C.OffsetRange{ + lower_offset: C.u64(offsetRange.Start), + upper_offset: C.u64(offsetRange.End), + program_index: C.u16(ebpfProgIndex), + } + if err := impl.interpreterOffsets.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), + cebpf.UpdateAny); err != nil { + log.Fatalf("Failed to place interpreter range in map: %v", err) + } + } + + return nil +} + +// 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) { + switch typ { + case libpf.Perl: + return impl.perlProcs, nil + case libpf.Python: + return impl.pyProcs, nil + case libpf.HotSpot: + 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 + 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, + data unsafe.Pointer) error { + log.Debugf("Loading symbol addresses into eBPF map for PID %d type %d", + pid, typ) + ebpfMap, err := impl.getInterpreterTypeMap(typ) + if err != nil { + return err + } + + pid32 := uint32(pid) + if err := ebpfMap.Update(unsafe.Pointer(&pid32), data, cebpf.UpdateAny); err != nil { + return fmt.Errorf("failed to add %v info: %s", typ, err) + } + return nil +} + +// 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 { + log.Debugf("Removing symbol addresses from eBPF map for PID %d type %d", + pid, typ) + ebpfMap, err := impl.getInterpreterTypeMap(typ) + if err != nil { + return err + } + + pid32 := uint32(pid) + if err := ebpfMap.Delete(unsafe.Pointer(&pid32)); err != nil { + return fmt.Errorf("failed to remove info: %v", err) + } + return nil +} + +// 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, + 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. + bePid := bits.ReverseBytes32(uint32(pid)) + bePage := bits.ReverseBytes64(prefix.Key) + + cKey := C.PIDPage{ + prefixLen: C.u32(support.BitWidthPID + prefix.Length), + pid: C.u32(bePid), + page: C.u64(bePage), + } + biasAndUnwindProgram, err := support.EncodeBiasAndUnwindProgram(bias, interpreterProgram) + if err != nil { + return err + } + + cValue := C.PIDPageMappingInfo{ + file_id: C.u64(fileID), + bias_and_unwind_program: C.u64(biasAndUnwindProgram), + } + + return impl.pidPageToMappingInfo.Update(unsafe.Pointer(&cKey), unsafe.Pointer(&cValue), + cebpf.UpdateNoExist) +} + +// DeletePidInterpreterMapping removes the element specified by pid, prefix and a corresponding +// 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 { + // pidPageToMappingInfo is a LPM trie and expects the pid and page + // to be in big endian format. + bePid := bits.ReverseBytes32(uint32(pid)) + bePage := bits.ReverseBytes64(prefix.Key) + + cKey := C.PIDPage{ + prefixLen: C.u32(support.BitWidthPID + prefix.Length), + pid: C.u32(bePid), + page: C.u64(bePage), + } + return impl.pidPageToMappingInfo.Delete(unsafe.Pointer(&cKey)) +} + +// trackMapError is a wrapper to report issues with changes to eBPF maps. +func (impl *ebpfMapsImpl) trackMapError(id metrics.MetricID, err error) error { + if err != nil { + impl.errCounterLock.Lock() + impl.errCounter[id]++ + impl.errCounterLock.Unlock() + } + return err +} + +// CollectMetrics returns gathered errors for changes to eBPF maps. +func (impl *ebpfMapsImpl) CollectMetrics() []metrics.Metric { + impl.errCounterLock.Lock() + defer impl.errCounterLock.Unlock() + + counts := make([]metrics.Metric, 0, 7) + for id, value := range impl.errCounter { + counts = append(counts, metrics.Metric{ + ID: id, + Value: metrics.MetricValue(value), + }) + // As we don't want to report metrics with zero values on the next call, + // we delete the entries from the map instead of just resetting them. + delete(impl.errCounter, id) + } + + return counts +} + +// poolPIDPage caches reusable heap-allocated C.PIDPage instances +// to avoid excessive heap allocations. +var poolPIDPage = sync.Pool{ + New: func() any { + return new(C.PIDPage) + }, +} + +// getPIDPage initializes a C.PIDPage instance. +func getPIDPage(pid libpf.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{ + pid: C.u32(bits.ReverseBytes32(uint32(pid))), + page: C.u64(bits.ReverseBytes64(prefix.Key)), + prefixLen: C.u32(support.BitWidthPID + prefix.Length), + } +} + +// 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 { + cPIDPage := poolPIDPage.Get().(*C.PIDPage) + *cPIDPage = getPIDPage(pid, prefix) + return cPIDPage +} + +// poolPIDPageMappingInfo caches reusable heap-allocated PIDPageMappingInfo instances +// to avoid excessive heap allocations. +var poolPIDPageMappingInfo = sync.Pool{ + New: func() any { + return new(C.PIDPageMappingInfo) + }, +} + +// getPIDPageMappingInfo returns a heap-allocated and initialized C.PIDPageMappingInfo instance. +// After usage, put the instance back into the pool with poolPIDPageMappingInfo.Put(). +func getPIDPageMappingInfo(fileID, biasAndUnwindProgram uint64) *C.PIDPageMappingInfo { + cInfo := poolPIDPageMappingInfo.Get().(*C.PIDPageMappingInfo) + cInfo.file_id = C.u64(fileID) + cInfo.bias_and_unwind_program = C.u64(biasAndUnwindProgram) + + return cInfo +} + +// probeBatchOperations tests if the BPF syscall accepts batch operations. +func probeBatchOperations(mapType cebpf.MapType) bool { + restoreRlimit, err := rlimit.MaximizeMemlock() + if errors.Is(err, unix.EPERM) { + // 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) + } + defer restoreRlimit() + + updates := 5 + probeMap, err := cebpf.NewMap(&cebpf.MapSpec{ + Type: mapType, + KeySize: 8, + ValueSize: 8, + MaxEntries: uint32(updates), + Flags: unix.BPF_F_NO_PREALLOC, + }) + if err != nil { + log.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) + + 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) { + // Older kernel do not support batch operations on maps. + // This is just fine and we return here. + return false + } + if n != updates || err != nil { + log.Errorf("Unexpected batch update error: %v", err) + return false + } + + // 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 + } + return true +} + +// getMapID returns the mapID number to use for given number of stack deltas. +func getMapID(numDeltas uint32) (uint16, error) { + significantBits := 32 - bits.LeadingZeros32(numDeltas) + if significantBits <= support.StackDeltaBucketSmallest { + return support.StackDeltaBucketSmallest, nil + } + if significantBits > support.StackDeltaBucketLargest { + return 0, fmt.Errorf("no map available for %d stack deltas", numDeltas) + } + return uint16(significantBits), nil +} + +// getOuterMap is a helper function to select the correct outer map for +// storing the stack deltas based on the mapID. +func (impl *ebpfMapsImpl) getOuterMap(mapID uint16) *cebpf.Map { + if mapID < support.StackDeltaBucketSmallest || + mapID > support.StackDeltaBucketLargest { + return nil + } + return impl.exeIDToStackDeltaMaps[mapID-support.StackDeltaBucketSmallest] +} + +// 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) { + key := uint32(pid) + _ = impl.reportedPIDs.Delete(unsafe.Pointer(&key)) +} + +// UpdateUnwindInfo writes UnwindInfo into the unwind info array at the given index +func (impl *ebpfMapsImpl) UpdateUnwindInfo(index uint16, info sdtypes.UnwindInfo) error { + if uint32(index) >= impl.unwindInfoArray.MaxEntries() { + return fmt.Errorf("unwind info array full (%d/%d items)", + index, impl.unwindInfoArray.MaxEntries()) + } + + key := C.u32(index) + value := C.UnwindInfo{ + opcode: C.u8(info.Opcode), + fpOpcode: C.u8(info.FPOpcode), + mergeOpcode: C.u8(info.MergeOpcode), + param: C.s32(info.Param), + fpParam: C.s32(info.FPParam), + } + return impl.trackMapError(metrics.IDUnwindInfoArrayUpdate, + impl.unwindInfoArray.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), + cebpf.UpdateAny)) +} + +// UpdateExeIDToStackDeltas creates a nested map for fileID in the eBPF map exeIDTostack_deltas +// and inserts the elements of the deltas array in this nested map. Returns mapID or error. +func (impl *ebpfMapsImpl) UpdateExeIDToStackDeltas(fileID host.FileID, deltas []StackDeltaEBPF) ( + uint16, error) { + numDeltas := len(deltas) + mapID, err := getMapID(uint32(numDeltas)) + if err != nil { + return 0, err + } + outerMap := impl.getOuterMap(mapID) + + keySize := uint32(C.sizeof_uint32_t) + valueSize := uint32(C.sizeof_StackDelta) + + restoreRlimit, err := rlimit.MaximizeMemlock() + if err != nil { + return 0, fmt.Errorf("failed to increase rlimit: %v", err) + } + defer restoreRlimit() + innerMap, err := cebpf.NewMap(&cebpf.MapSpec{ + Type: cebpf.Array, + KeySize: keySize, + ValueSize: valueSize, + MaxEntries: 1 << mapID, + }) + if err != nil { + return 0, fmt.Errorf("failed to create inner map: %v", err) + } + defer func() { + if err = innerMap.Close(); err != nil { + log.Errorf("Failed to close FD of inner map for 0x%x: %v", fileID, err) + } + }() + + 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)) + } + + if impl.hasGenericBatchOperations { + innerKeys := make([]uint32, numDeltas) + stackDeltas := make([]C.StackDelta, numDeltas) + + // Prepare values for batch update. + for index, delta := range deltas { + innerKeys[index] = uint32(index) + stackDeltas[index].addrLow = C.uint16_t(delta.AddressLow) + stackDeltas[index].unwindInfo = C.uint16_t(delta.UnwindInfo) + } + + _, err := innerMap.BatchUpdate( + ptrCastMarshaler[uint32](innerKeys), + ptrCastMarshaler[C.StackDelta](stackDeltas), + &cebpf.BatchOptions{Flags: uint64(cebpf.UpdateAny)}) + if err != nil { + return 0, impl.trackMapError(metrics.IDExeIDToStackDeltasBatchUpdate, + fmt.Errorf("failed to batch insert %d elements for 0x%x "+ + "into exeIDTostack_deltas: %v", + numDeltas, fileID, err)) + } + return mapID, nil + } + + innerKey := uint32(0) + stackDelta := C.StackDelta{} + for index, delta := range deltas { + stackDelta.addrLow = C.uint16_t(delta.AddressLow) + stackDelta.unwindInfo = C.uint16_t(delta.UnwindInfo) + innerKey = uint32(index) + if err := innerMap.Update(unsafe.Pointer(&innerKey), unsafe.Pointer(&stackDelta), + cebpf.UpdateAny); err != nil { + return 0, impl.trackMapError(metrics.IDExeIDToStackDeltasUpdate, fmt.Errorf( + "failed to insert element %d for 0x%x into exeIDTostack_deltas: %v", + index, fileID, err)) + } + } + + return mapID, nil +} + +// DeleteExeIDToStackDeltas removes all eBPF stack delta entries for given fileID and mapID number. +func (impl *ebpfMapsImpl) DeleteExeIDToStackDeltas(fileID host.FileID, mapID uint16) error { + outerMap := impl.getOuterMap(mapID) + if outerMap == nil { + return fmt.Errorf("invalid mapID %d", mapID) + } + + // 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))) +} + +// UpdateStackDeltaPages adds fileID/page with given information to eBPF map. If the entry exists, +// it will return an error. Otherwise the key/value pairs will be appended to the hash. +func (impl *ebpfMapsImpl) UpdateStackDeltaPages(fileID host.FileID, numDeltasPerPage []uint16, + mapID uint16, firstPageAddr uint64) error { + firstDelta := uint32(0) + keys := make([]C.StackDeltaPageKey, len(numDeltasPerPage)) + values := make([]C.StackDeltaPageInfo, len(numDeltasPerPage)) + + // Prepare the key/value combinations that will be loaded. + for pageNumber, numDeltas := range numDeltasPerPage { + pageAddr := firstPageAddr + uint64(pageNumber)<= 512kB. + minimumMemoizableGapSize = 512 * 1024 +) + +// ExecutableInfo stores information about an executable (ELF file). +type ExecutableInfo struct { + // Data stores per-executable interpreter information if the file ID that this + // instance belongs to was previously identified as an interpreter. Otherwise, + // this field is nil. + Data interpreter.Data + // TSDInfo stores TSD information if the executable is libc, otherwise nil. + TSDInfo *tpbase.TSDInfo +} + +// ExecutableInfoManager manages all per-executable (FileID) information that we require to +// perform our native and interpreter unwinding. Executable information is de-duplicated between +// processes and is kept around as long as there is at least one process that is known to have +// the corresponding FileID loaded (reference counting). Tracking loaded executables is left to +// the caller. +// +// The manager is synchronized internally and all public methods can be called from an arbitrary +// number of threads simultaneously. +// +// The manager is responsible for managing entries in the following BPF maps: +// +// - stack_delta_page_to_info +// - exe_id_to_%d_stack_deltas +// - unwind_info_array +// - interpreter_offsets +// +// All of these maps can be read by anyone, but are written to exclusively by this manager. +type ExecutableInfoManager struct { + // sdp allows fetching stack deltas for executables. + sdp nativeunwind.StackDeltaProvider + + // state bundles up all mutable state of the manager. + state xsync.RWMutex[executableInfoManagerState] +} + +// NewExecutableInfoManager creates a new instance of the executable info manager. +func NewExecutableInfoManager( + sdp nativeunwind.StackDeltaProvider, + ebpf pmebpf.EbpfHandler, + includeTracers []bool, +) *ExecutableInfoManager { + // Initialize interpreter loaders. + interpreterLoaders := make([]interpreter.Loader, 0) + if includeTracers[config.PerlTracer] { + interpreterLoaders = append(interpreterLoaders, perl.Loader) + } + if includeTracers[config.PythonTracer] { + interpreterLoaders = append(interpreterLoaders, python.Loader) + } + if includeTracers[config.PHPTracer] { + interpreterLoaders = append(interpreterLoaders, php.Loader, phpjit.Loader) + } + if includeTracers[config.HotspotTracer] { + interpreterLoaders = append(interpreterLoaders, hotspot.Loader) + } + if includeTracers[config.RubyTracer] { + interpreterLoaders = append(interpreterLoaders, ruby.Loader) + } + if includeTracers[config.V8Tracer] { + interpreterLoaders = append(interpreterLoaders, nodev8.Loader) + } + + return &ExecutableInfoManager{ + sdp: sdp, + state: xsync.NewRWMutex(executableInfoManagerState{ + interpreterLoaders: interpreterLoaders, + executables: map[host.FileID]*entry{}, + unwindInfoIndex: map[sdtypes.UnwindInfo]uint16{}, + ebpf: ebpf, + }), + } +} + +// AddOrIncRef either adds information about an executable to the internal cache (when first +// encountering it) or increments the reference count if the executable is already known. +// +// The return value is copied instead of returning a pointer in order to spare us the use +// of getters and more complicated locking semantics. +func (mgr *ExecutableInfoManager) AddOrIncRef(fileID host.FileID, + elfRef *pfelf.Reference) (ExecutableInfo, error) { + var ( + intervalData sdtypes.IntervalData + tsdInfo *tpbase.TSDInfo + ref mapRef + gaps []libpf.Range + err error + ) + + // Fast path for executable info that is already present. + state := mgr.state.WLock() + info, ok := state.executables[fileID] + if ok { + defer mgr.state.WUnlock(&state) + info.rc++ + return info.ExecutableInfo, nil + } + + // Otherwise, gather interval data via SDP. This can take a while, + // so we release the lock before doing this. + mgr.state.WUnlock(&state) + + if err = mgr.sdp.GetIntervalStructuresForFile(fileID, elfRef, &intervalData); err != nil { + return ExecutableInfo{}, fmt.Errorf("failed to extract interval data: %w", err) + } + + // Also gather TSD info if applicable. + if tpbase.IsPotentialTSDDSO(elfRef.FileName()) { + if ef, errx := elfRef.GetELF(); errx == nil { + tsdInfo, _ = tpbase.ExtractTSDInfo(ef) + } + } + + // Re-take the lock and check whether another thread beat us to + // inserting the data while we were waiting for the write lock. + state = mgr.state.WLock() + defer mgr.state.WUnlock(&state) + if info, ok = state.executables[fileID]; ok { + info.rc++ + return info.ExecutableInfo, nil + } + + // Load the data into BPF maps. + ref, gaps, err = state.loadDeltas(fileID, intervalData.Deltas) + if err != nil { + return ExecutableInfo{}, fmt.Errorf("failed to load deltas: %w", err) + } + + // Create the LoaderInfo for interpreter detection + loaderInfo := interpreter.NewLoaderInfo(fileID, elfRef, gaps) + + // Insert a corresponding record into our map. + info = &entry{ + ExecutableInfo: ExecutableInfo{ + Data: state.detectAndLoadInterpData(loaderInfo), + TSDInfo: tsdInfo, + }, + mapRef: ref, + rc: 1, + } + state.executables[fileID] = info + + return info.ExecutableInfo, nil +} + +// AddSynthIntervalData should only be called once for a given file ID. It will error if it or +// AddOrIncRef has been previously called for the same file ID. Interpreter detection is skipped. +func (mgr *ExecutableInfoManager) AddSynthIntervalData( + fileID host.FileID, + data sdtypes.IntervalData, +) error { + state := mgr.state.WLock() + defer mgr.state.WUnlock(&state) + + if _, exists := state.executables[fileID]; exists { + return fmt.Errorf("AddSynthIntervalData: mapping already exists") + } + + ref, _, err := state.loadDeltas(fileID, data.Deltas) + if err != nil { + return fmt.Errorf("failed to load deltas: %w", err) + } + + state.executables[fileID] = &entry{ + ExecutableInfo: ExecutableInfo{Data: nil}, + mapRef: ref, + rc: 1, + } + + return nil +} + +// RemoveOrDecRef decrements the reference counter of the executable being tracked. Once the RC +// reaches zero, information about the file is removed from the manager and the corresponding +// BPF maps. +func (mgr *ExecutableInfoManager) RemoveOrDecRef(fileID host.FileID) error { + state := mgr.state.WLock() + defer mgr.state.WUnlock(&state) + + info, ok := state.executables[fileID] + if !ok { + return fmt.Errorf("FileID %v is not known to ExecutableInfoManager", fileID) + } + + switch info.rc { + case 1: + // This was the last reference: clean up all associated resources. + if err := state.unloadDeltas(fileID, &info.mapRef); err != nil { + return fmt.Errorf("failed remove fileID 0x%x from BPF maps: %w", fileID, err) + } + delete(state.executables, fileID) + case 0: + // This should be unreachable. + return fmt.Errorf("state corruption in ExecutableInfoManager: encountered 0 RC") + default: + info.rc-- + } + + return nil +} + +// NumInterpreterLoaders returns the number of interpreter loaders that are enabled. +func (mgr *ExecutableInfoManager) NumInterpreterLoaders() int { + state := mgr.state.RLock() + defer mgr.state.RUnlock(&state) + return len(state.interpreterLoaders) +} + +// UpdateMetricSummary updates the metrics in the given metric map. +func (mgr *ExecutableInfoManager) UpdateMetricSummary(summary metrics.Summary) { + state := mgr.state.RLock() + summary[metrics.IDNumExeIDLoadedToEBPF] = + metrics.MetricValue(len(state.executables)) + summary[metrics.IDUnwindInfoArraySize] = + metrics.MetricValue(len(state.unwindInfoIndex)) + summary[metrics.IDHashmapNumStackDeltaPages] = + metrics.MetricValue(state.numStackDeltaMapPages) + mgr.state.RUnlock(&state) + + deltaProviderStatistics := mgr.sdp.GetAndResetStatistics() + summary[metrics.IDStackDeltaProviderCacheHit] = + metrics.MetricValue(deltaProviderStatistics.Hit) + summary[metrics.IDStackDeltaProviderCacheMiss] = + metrics.MetricValue(deltaProviderStatistics.Miss) + summary[metrics.IDStackDeltaProviderExtractionError] = + metrics.MetricValue(deltaProviderStatistics.ExtractionErrors) +} + +type executableInfoManagerState struct { + // interpreterLoaders is a list of instances of an interface that provide functionality + // for loading the host agent support for a specific interpreter type. + interpreterLoaders []interpreter.Loader + + // ebpf provides the interface to manipulate eBPF maps. + ebpf pmebpf.EbpfHandler + + // executables is the primary mapping from file ID to executable information. Entries are + // managed with reference counting and are synchronized with various eBPF maps: + // + // - stack_delta_page_to_info + // - exe_id_to_%d_stack_deltas + executables map[host.FileID]*entry + + // unwindInfoIndex maps each unique UnwindInfo to its array index within the corresponding + // BPF map. This serves for de-duplication purposes. Elements are never removed. Entries are + // synchronized with the unwind_info_array eBPF map. + unwindInfoIndex map[sdtypes.UnwindInfo]uint16 + + // numStackDeltaMapPages tracks the current size of the corresponding eBPF map. + numStackDeltaMapPages uint64 +} + +// detectAndLoadInterpData attempts to detect the given executable as an interpreter. If detection +// succeeds, it then loads additional per-interpreter data into the BPF maps and returns the +// interpreter data. +func (state *executableInfoManagerState) detectAndLoadInterpData( + loaderInfo *interpreter.LoaderInfo) interpreter.Data { + // Ask all interpreter loaders whether they want to handle this executable. + for _, loader := range state.interpreterLoaders { + data, err := loader(state.ebpf, loaderInfo) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // Very common if the process exited when we tried to analyze it. + log.Debugf("Failed to load %v (%#016x): file not found", + loaderInfo.FileName(), loaderInfo.FileID()) + } else { + log.Errorf("Failed to load %v (%#016x): %v", + loaderInfo.FileName(), loaderInfo.FileID(), err) + } + return nil + } + if data == nil { + continue + } + + log.Debugf("Interpreter data %v for %v (%#016x)", + data, loaderInfo.FileName(), loaderInfo.FileID()) + return data + } + + return nil +} + +// loadDeltas converts the sdtypes.StackDelta to StackDeltaEBPF and passes that to +// the ebpf interface to be loaded to kernel maps. While converting the deltas, it +// also creates a list of all large gaps in the executable. +func (state *executableInfoManagerState) loadDeltas( + fileID host.FileID, + deltas []sdtypes.StackDelta, +) (ref mapRef, gaps []libpf.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 + } + + firstPage := deltas[0].Address >> support.StackDeltaPageBits + firstPageAddr := deltas[0].Address &^ support.StackDeltaPageMask + lastPage := deltas[numDeltas-1].Address >> support.StackDeltaPageBits + numPages := lastPage - firstPage + 1 + numDeltasPerPage := make([]uint16, numPages) + + // Index the unwind-info. + var unwindInfo sdtypes.UnwindInfo + ebpfDeltas := make([]pmebpf.StackDeltaEBPF, 0, numDeltas) + for index, delta := range deltas { + if unwindInfo.MergeOpcode != 0 { + // This delta was merged in the previous iteration. + unwindInfo.MergeOpcode = 0 + continue + } + unwindInfo = delta.Info + if index+1 < len(deltas) { + unwindInfo.MergeOpcode = calculateMergeOpcode(delta, deltas[index+1]) + nextDeltaAddr := deltas[index+1].Address + if delta.Hints&sdtypes.UnwindHintGap != 0 && + 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{ + Start: delta.Address, + End: nextDeltaAddr}) + } + } + // Uses the new 'unwindInfo' with potentially updated MergeOpcode + // here. In the end, it's only the unwindInfoIndex being different for + // merged deltas. + var unwindInfoIndex uint16 + unwindInfoIndex, err = state.getUnwindInfoIndex(unwindInfo) + if err != nil { + return mapRef{}, nil, err + } + ebpfDeltas = append(ebpfDeltas, pmebpf.StackDeltaEBPF{ + AddressLow: uint16(delta.Address), + UnwindInfo: unwindInfoIndex, + }) + numDeltasPerPage[(delta.Address>>support.StackDeltaPageBits)-firstPage]++ + } + + // Update data to eBPF + mapID, err := state.ebpf.UpdateExeIDToStackDeltas(fileID, ebpfDeltas) + if err != nil { + return mapRef{}, nil, + fmt.Errorf("failed UpdateExeIDToStackDeltas for FileID %x: %v", fileID, err) + } + + // Update stack delta pages + if err = state.ebpf.UpdateStackDeltaPages(fileID, numDeltasPerPage, mapID, + firstPageAddr); err != nil { + _ = state.ebpf.DeleteExeIDToStackDeltas(fileID, ref.MapID) + return mapRef{}, nil, + fmt.Errorf("failed UpdateStackDeltaPages for FileID %x: %v", fileID, err) + } + state.numStackDeltaMapPages += numPages + + return mapRef{ + MapID: mapID, + StartPage: firstPageAddr, + NumPages: uint32(numPages), + }, gaps, nil +} + +// calculateMergeOpcode calculates the merge opcode byte given two consecutive StackDeltas. +// Zero means no merging happened. Only small differences for address and the CFA delta +// are considered, in order to limit the amount of unique combinations generated. +func calculateMergeOpcode(delta, nextDelta sdtypes.StackDelta) uint8 { + if delta.Info.Opcode == sdtypes.UnwindOpcodeCommand { + return 0 + } + addrDiff := nextDelta.Address - delta.Address + if addrDiff < 1 || addrDiff > 2 { + return 0 + } + if nextDelta.Info.Opcode != delta.Info.Opcode || + nextDelta.Info.FPOpcode != delta.Info.FPOpcode || + nextDelta.Info.FPParam != delta.Info.FPParam { + return 0 + } + paramDiff := nextDelta.Info.Param - delta.Info.Param + switch paramDiff { + case 8: + return uint8(addrDiff) + case -8: + return uint8(addrDiff) | support.MergeOpcodeNegative + } + return 0 +} + +// getUnwindInfoIndex maps the given UnwindInfo to its eBPF array index. This can be direct +// encoding, or index to the unwind info array (new index is created if needed). +// See STACK_DELTA_COMMAND_FLAG for further explanation of the directly encoded unwind infos. +func (state *executableInfoManagerState) getUnwindInfoIndex( + info sdtypes.UnwindInfo, +) (uint16, error) { + if info.Opcode == sdtypes.UnwindOpcodeCommand { + return uint16(info.Param) | support.DeltaCommandFlag, nil + } + + if index, ok := state.unwindInfoIndex[info]; ok { + return index, nil + } + index := uint16(len(state.unwindInfoIndex)) + if err := state.ebpf.UpdateUnwindInfo(index, info); err != nil { + return 0, fmt.Errorf("failed to insert unwind info #%d: %v", index, err) + } + state.unwindInfoIndex[info] = index + return index, nil +} + +// unloadDeltas removes information that was previously added by loadDeltas from our BPF maps. +func (state *executableInfoManagerState) unloadDeltas( + fileID host.FileID, + ref *mapRef, +) error { + if ref.MapID == 0 { + // Nothing to do: no data was inserted in the first place. + return nil + } + + // To avoid race conditions first remove the stack delta page mappings + // which reference the stack delta data. + var err error + for i := uint64(0); i < uint64(ref.NumPages); i++ { + pageAddr := ref.StartPage + i< %#x", key, val) +} + +var _ FileIDMapper = (*lruFileIDMapper)(nil) + +// MapFileIDMapper implements the FileIDMApper using a map (for testing) +type MapFileIDMapper struct { + fileMap map[host.FileID]libpf.FileID +} + +func NewMapFileIDMapper() *MapFileIDMapper { + return &MapFileIDMapper{ + fileMap: make(map[host.FileID]libpf.FileID), + } +} + +func (fm *MapFileIDMapper) Get(key host.FileID) (libpf.FileID, bool) { + if value, ok := fm.fileMap[key]; ok { + return value, true + } + return libpf.FileID{}, true +} + +func (fm *MapFileIDMapper) Set(key host.FileID, value libpf.FileID) { + fm.fileMap[key] = value +} + +var _ FileIDMapper = (*MapFileIDMapper)(nil) + +// FileIDMapper is responsible for mapping between 64-bit file IDs to 128-bit file IDs. The file ID +// mappings are inserted typically at the same time the files are hashed. The 128-bit file IDs +// are retrieved prior to reporting requests to the collection agent. +type FileIDMapper interface { + // Get retrieves the 128-bit file ID for the provided 64-bit file ID. Otherwise, + // the second return value is false. + Get(pre host.FileID) (libpf.FileID, bool) + // Set adds a mapping from the 64-bit file ID to the 128-bit file ID. + Set(pre host.FileID, post libpf.FileID) +} diff --git a/processmanager/manager.go b/processmanager/manager.go new file mode 100644 index 00000000..47df281a --- /dev/null +++ b/processmanager/manager.go @@ -0,0 +1,326 @@ +/* + * 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 processmanager manages the loading and unloading of information related to processes. +package processmanager + +import ( + "context" + "errors" + "fmt" + "time" + + lru "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/libpf/nativeunwind" + sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/libpf/periodiccaller" + "github.com/elastic/otel-profiling-agent/libpf/traceutil" + "github.com/elastic/otel-profiling-agent/lpm" + "github.com/elastic/otel-profiling-agent/metrics" + pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" + eim "github.com/elastic/otel-profiling-agent/processmanager/execinfomanager" + "github.com/elastic/otel-profiling-agent/reporter" +) + +const ( + // lruFileIDCacheSize is the LRU size for caching 64-bit and 128-bit file IDs. + // This should reflect the number of hot file IDs that are seen often in a trace. + lruFileIDCacheSize = 32768 + + // Maximum size of the LRU cache holding the executables' ELF information. + elfInfoCacheSize = 16384 + + // TTL of entries in the LRU cache holding the executables' ELF information. + elfInfoCacheTTL = 6 * time.Hour +) + +var ( + // dummyPrefix is the LPM prefix installed to indicate the process is known + dummyPrefix = lpm.Prefix{Key: 0, Length: 64} +) + +var ( + errSymbolizationNotSupported = errors.New("symbolization not supported") + // errUnknownMapping indicates that the memory mapping is not known to + // the process manager. + errUnknownMapping = errors.New("unknown memory mapping") + // errUnknownPID indicates that the process is not known to the process manager. + errUnknownPID = errors.New("unknown process") +) + +// New creates a new ProcessManager which is responsible for keeping track of loading +// and unloading of symbols for processes. +// Four external interfaces are used to access the processes and related resources: ebpf, +// fileIDMapper, opener and reportFrameMetadata. Specify 'nil' for these interfaces to use +// the default implementation. +func New(ctx context.Context, includeTracers []bool, monitorInterval time.Duration, + ebpf pmebpf.EbpfHandler, fileIDMapper FileIDMapper, symbolReporter reporter.SymbolReporter, + sdp nativeunwind.StackDeltaProvider, filterErrorFrames bool) (*ProcessManager, error) { + if fileIDMapper == nil { + var err error + fileIDMapper, err = newFileIDMapper(lruFileIDCacheSize) + if err != nil { + return nil, fmt.Errorf("failed to initialize file ID mapping: %v", err) + } + } + + elfInfoCache, err := lru.New[libpf.OnDiskFileIdentifier, elfInfo](elfInfoCacheSize, + libpf.OnDiskFileIdentifier.Hash32) + if err != nil { + return nil, fmt.Errorf("unable to create elfInfoCache: %v", err) + } + elfInfoCache.SetLifetime(elfInfoCacheTTL) + + em := eim.NewExecutableInfoManager(sdp, ebpf, includeTracers) + + interpreters := make(map[libpf.PID]map[libpf.OnDiskFileIdentifier]interpreter.Instance) + + pm := &ProcessManager{ + interpreterTracerEnabled: em.NumInterpreterLoaders() > 0, + eim: em, + interpreters: interpreters, + exitEvents: make(map[libpf.PID]libpf.KTime), + pidToProcessInfo: make(map[libpf.PID]*processInfo), + ebpf: ebpf, + FileIDMapper: fileIDMapper, + elfInfoCache: elfInfoCache, + reporter: symbolReporter, + metricsAddSlice: metrics.AddSlice, + filterErrorFrames: filterErrorFrames, + } + + collectInterpreterMetrics(ctx, pm, monitorInterval) + + return pm, nil +} + +// metricSummaryToSlice creates a metrics.Metric slice from a map of metric IDs to values. +func metricSummaryToSlice(summary metrics.Summary) []metrics.Metric { + result := make([]metrics.Metric, 0, len(summary)) + for mID, mVal := range summary { + result = append(result, metrics.Metric{ID: mID, Value: mVal}) + } + return result +} + +// updateMetricSummary gets the metrics from the provided interpreter instance and updaates the +// provided summary by aggregating the new metrics into the summary. +// The caller is responsible to hold the lock on the interpreter.Instance to avoid race conditions. +func updateMetricSummary(ii interpreter.Instance, summary metrics.Summary) error { + instanceMetrics, err := ii.GetAndResetMetrics() + if err != nil { + return err + } + + for _, metric := range instanceMetrics { + summary[metric.ID] += metric.Value + } + + return nil +} + +// collectInterpreterMetrics starts a goroutine that periodically fetches and reports interpreter +// metrics. +func collectInterpreterMetrics(ctx context.Context, pm *ProcessManager, + monitorInterval time.Duration) { + periodiccaller.Start(ctx, monitorInterval, func() { + pm.mu.RLock() + defer pm.mu.RUnlock() + + summary := make(map[metrics.MetricID]metrics.MetricValue) + + for pid := range pm.interpreters { + for addr := range pm.interpreters[pid] { + if err := updateMetricSummary(pm.interpreters[pid][addr], summary); err != nil { + log.Errorf("Failed to get/reset metrics for PID %d at 0x%x: %v", + pid, addr, err) + } + } + } + + summary[metrics.IDHashmapPidPageToMappingInfo] = + metrics.MetricValue(pm.pidPageToMappingInfoSize) + + summary[metrics.IDELFInfoCacheHit] = + metrics.MetricValue(pm.elfInfoCacheHit.Swap(0)) + summary[metrics.IDELFInfoCacheMiss] = + metrics.MetricValue(pm.elfInfoCacheMiss.Swap(0)) + + summary[metrics.IDErrProcNotExist] = + metrics.MetricValue(pm.mappingStats.errProcNotExist.Swap(0)) + summary[metrics.IDErrProcESRCH] = + metrics.MetricValue(pm.mappingStats.errProcESRCH.Swap(0)) + summary[metrics.IDErrProcPerm] = + metrics.MetricValue(pm.mappingStats.errProcPerm.Swap(0)) + summary[metrics.IDNumProcAttempts] = + metrics.MetricValue(pm.mappingStats.numProcAttempts.Swap(0)) + summary[metrics.IDMaxProcParseUsec] = + metrics.MetricValue(pm.mappingStats.maxProcParseUsec.Swap(0)) + summary[metrics.IDTotalProcParseUsec] = + metrics.MetricValue(pm.mappingStats.totalProcParseUsec.Swap(0)) + + mapsMetrics := pm.ebpf.CollectMetrics() + for _, metric := range mapsMetrics { + summary[metric.ID] = metric.Value + } + + pm.eim.UpdateMetricSummary(summary) + pm.metricsAddSlice(metricSummaryToSlice(summary)) + }) +} + +func (pm *ProcessManager) Close() { +} + +func (pm *ProcessManager) symbolizeFrame(frame int, trace *host.Trace, + newTrace *libpf.Trace) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if len(pm.interpreters[trace.PID]) == 0 { + return fmt.Errorf("interpreter process gone") + } + + for _, instance := range pm.interpreters[trace.PID] { + if err := instance.Symbolize(pm.reporter, &trace.Frames[frame], newTrace); err != nil { + if errors.Is(err, interpreter.ErrMismatchInterpreterType) { + // The interpreter type of instance did not match the type of frame. + // So continue with the next interpreter instance for this PID. + continue + } + return fmt.Errorf("symbolization failed: %w", err) + } + return nil + } + + return fmt.Errorf("no matching interpreter instance (of len %d): %w", + len(pm.interpreters[trace.PID]), errSymbolizationNotSupported) +} + +func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace) { + traceLen := len(trace.Frames) + + newTrace = &libpf.Trace{ + Files: make([]libpf.FileID, 0, traceLen), + Linenos: make([]libpf.AddressOrLineno, 0, traceLen), + FrameTypes: make([]libpf.FrameType, 0, traceLen), + } + + for i := 0; i < traceLen; i++ { + frame := &trace.Frames[i] + + if frame.Type.IsError() { + if !pm.filterErrorFrames { + newTrace.AppendFrame(frame.Type, libpf.UnsymbolizedFileID, frame.Lineno) + } + continue + } + + switch frame.Type.Interpreter() { + case libpf.UnknownInterp: + log.Errorf("Unexpected frame type 0x%02X (neither error nor interpreter frame)", + uint8(frame.Type)) + case libpf.Native, libpf.Kernel: + // When unwinding stacks, the address is obtained from the stack + // which contains pointer to the *next* instruction to be executed. + // + // For all kernel frames, the kernel unwinder will always produce + // a frame in which the RIP is after a call instruction (it hides the top + // frames that leads to the unwinder itself). + // + // For leaf user mode frames (without kernel frames) the RIP from + // our unwinder is good as is, and must not be altered because the + // previous instruction address is unknown -- we might have just + // executed a jump or a call that got us to the address found in + // these frames. + // + // For other user mode frames we are at the next instruction after a + // call. And often the next instruction is already part of the next + // source code line's debug info areas. So we need to fixup the non-top + // frames so that we get source code lines pointing to the call instruction. + // We would ideally wish to subtract the size of the instruction from + // the return address we retrieved - but the size of calls can vary + // (indirect calls etc.). If, on the other hand, we subtract 1 from + // the address, we ensure that we fall into the range of addresses + // associated with that function call in the debug information. + // + // The unwinder will produce stack traces like the following: + // + // Frame 0: + // bla %reg <- address of frame 0 + // retq + // + // Frame 1: + // call + // add %rax, %rbx <- address of frame 1 == return address of frame 0 + + relativeRIP := frame.Lineno + if i > 0 || frame.Type.IsInterpType(libpf.Kernel) { + relativeRIP-- + } + fileID, ok := pm.FileIDMapper.Get(frame.File) + if !ok { + log.Debugf( + "file ID lookup failed for PID %d, frame %d/%d, frame type %d", + trace.PID, i, traceLen, frame.Type) + + newTrace.AppendFrame(frame.Type, libpf.UnsymbolizedFileID, + libpf.AddressOrLineno(0)) + continue + } + newTrace.AppendFrame(frame.Type, fileID, relativeRIP) + default: + err := pm.symbolizeFrame(i, trace, newTrace) + if err != nil { + log.Debugf( + "symbolization failed for PID %d, frame %d/%d, frame type %d: %v", + trace.PID, i, traceLen, frame.Type, err) + + newTrace.AppendFrame(frame.Type, libpf.UnsymbolizedFileID, libpf.AddressOrLineno(0)) + } + } + } + newTrace.Hash = traceutil.HashTrace(newTrace) + return newTrace +} + +func (pm *ProcessManager) SymbolizationComplete(traceCaptureKTime libpf.KTime) { + pm.mu.Lock() + defer pm.mu.Unlock() + + nowKTime := libpf.GetKTime() + + for pid, pidExitKTime := range pm.exitEvents { + if pidExitKTime > traceCaptureKTime { + continue + } + for _, instance := range pm.interpreters[pid] { + if err := instance.Detach(pm.ebpf, pid); err != nil { + log.Errorf("Failed to handle interpreted process exit for PID %d: %v", + pid, err) + } + } + delete(pm.interpreters, pid) + delete(pm.exitEvents, pid) + + log.Debugf("PID %v exit latency %v ms", pid, (nowKTime-pidExitKTime)/1e6) + } +} + +// 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 +// file ID, the stack delta provider will not be consulted and the manually added stack deltas take +// precedence. +func (pm *ProcessManager) AddSynthIntervalData(fileID host.FileID, + data sdtypes.IntervalData) error { + return pm.eim.AddSynthIntervalData(fileID, data) +} diff --git a/processmanager/manager_test.go b/processmanager/manager_test.go new file mode 100644 index 00000000..73798550 --- /dev/null +++ b/processmanager/manager_test.go @@ -0,0 +1,636 @@ +/* + * 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 processmanager + +// See also utils/coredump/coredump_test.go for core dump based testing. + +import ( + "context" + "errors" + "fmt" + "math/rand" + "os" + "reflect" + "testing" + "time" + "unsafe" + + "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/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" + pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" +) + +// dummyProcess implements pfelf.Process for testing purposes +type dummyProcess struct { + pid libpf.PID +} + +func (d *dummyProcess) PID() libpf.PID { + return d.pid +} + +func (d *dummyProcess) GetMachineData() process.MachineData { + return process.MachineData{} +} + +func (d *dummyProcess) GetMappings() ([]process.Mapping, error) { + return nil, errors.New("not implemented") +} + +func (d *dummyProcess) GetThreads() ([]process.ThreadInfo, error) { + return nil, errors.New("not implemented") +} + +func (d *dummyProcess) GetRemoteMemory() remotememory.RemoteMemory { + return remotememory.RemoteMemory{} +} + +func (d *dummyProcess) GetMappingFile(_ *process.Mapping) string { + return "" +} + +func (d *dummyProcess) CalculateMappingFileID(m *process.Mapping) (libpf.FileID, error) { + return pfelf.CalculateID(m.Path) +} + +func (d *dummyProcess) OpenMappingFile(m *process.Mapping) (process.ReadAtCloser, error) { + return os.Open(m.Path) +} + +func (d *dummyProcess) OpenELF(name string) (*pfelf.File, error) { + return pfelf.Open(name) +} + +func (d *dummyProcess) Close() error { + return nil +} + +func newTestProcess(pid libpf.PID) process.Process { + return &dummyProcess{pid: pid} +} + +// dummyStackDeltaProvider is an implementation of nativeunwind.StackDeltaProvider. +// It is intended to be used only within this test. +type dummyStackDeltaProvider struct{} + +// GetIntervalStructuresForFile fills in the expected data structure with semi random data. +func (d *dummyStackDeltaProvider) GetIntervalStructuresForFile(_ host.FileID, + _ *pfelf.Reference, result *sdtypes.IntervalData) error { + // nolint:gosec + r := rand.New(rand.NewSource(42)) + addr := 0x10 + // nolint:gosec + for i := 0; i < r.Intn(42); i++ { + // nolint:gosec + addr += r.Intn(42 * 42) + // nolint:gosec + data := int32(8 * r.Intn(42)) + result.Deltas.Add(sdtypes.StackDelta{ + Address: uint64(addr), + Info: sdtypes.UnwindInfo{Opcode: sdtypes.UnwindOpcodeBaseSP, Param: data}, + }) + } + return nil +} + +// GetAndResetStatistics satisfies the interface and does not return values. +func (d *dummyStackDeltaProvider) GetAndResetStatistics() nativeunwind.Statistics { + return nativeunwind.Statistics{} +} + +// Compile time check that the dummyStackDeltaProvider implements its interface correctly. +var _ nativeunwind.StackDeltaProvider = (*dummyStackDeltaProvider)(nil) + +// generateDummyFiles creates num temporary files. The caller is responsible to delete +// these files afterwards. +func generateDummyFiles(t *testing.T, num int) []string { + t.Helper() + var files []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) + } + // 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) + } + files = append(files, tmpfile.Name()) + } + return files +} + +// mappingArgs provides a structured way for the arguments to NewMapping() +// for the tests. +type mappingArgs struct { + // pid represents the simulated process ID. + pid libpf.PID + // vaddr represents the simulated start of the mapped memory. + vaddr uint64 + // bias is the load bias to simulate and verify. + bias uint64 +} + +// ebpfMapsMockup implements the ebpf interface as test mockup +type ebpfMapsMockup struct { + updateProcCount, deleteProcCount uint8 + + stackDeltaMemory []pmebpf.StackDeltaEBPF + // deleteStackDeltaRangesCount reflects the number of times + // the deleteStackDeltaRanges to update the eBPF map was called. + deleteStackDeltaRangesCount uint8 + // deleteStackDeltaPage reflects the number of times + // the DeleteStackDeltaPage to update the eBPF map was called. + deleteStackDeltaPage uint8 + // deletePidPageMappingCount reflects the number of times + // the deletePidPageMapping to update the eBPF map was called. + deletePidPageMappingCount uint8 + // expectedBias value for updatedPidPageToExeIDOffset calls + expectedBias uint64 +} + +var _ interpreter.EbpfHandler = &ebpfMapsMockup{} + +func (mockup *ebpfMapsMockup) RemoveReportedPID(libpf.PID) { +} + +func (mockup *ebpfMapsMockup) UpdateInterpreterOffsets(uint16, host.FileID, []libpf.Range) error { + return nil +} + +func (mockup *ebpfMapsMockup) UpdateProcData(libpf.InterpType, libpf.PID, unsafe.Pointer) error { + mockup.updateProcCount++ + return nil +} + +func (mockup *ebpfMapsMockup) DeleteProcData(libpf.InterpType, libpf.PID) error { + mockup.deleteProcCount++ + return nil +} + +func (mockup *ebpfMapsMockup) UpdatePidInterpreterMapping(libpf.PID, + lpm.Prefix, uint8, host.FileID, uint64) error { + return nil +} + +func (mockup *ebpfMapsMockup) DeletePidInterpreterMapping(libpf.PID, lpm.Prefix) error { + return nil +} + +func (mockup *ebpfMapsMockup) UpdateUnwindInfo(uint16, sdtypes.UnwindInfo) error { return nil } + +func (mockup *ebpfMapsMockup) UpdateExeIDToStackDeltas(fileID host.FileID, + deltaArrays []pmebpf.StackDeltaEBPF) (uint16, error) { + mockup.stackDeltaMemory = append(mockup.stackDeltaMemory, deltaArrays...) + // execinfomanager expects a mapID >0. So to fake this behavior, we return + // parts of the fileID. + return uint16(fileID), nil +} + +func (mockup *ebpfMapsMockup) DeleteExeIDToStackDeltas(host.FileID, uint16) error { + mockup.deleteStackDeltaRangesCount++ + return nil +} + +func (mockup *ebpfMapsMockup) UpdateStackDeltaPages(host.FileID, []uint16, + uint16, uint64) error { + return nil +} + +func (mockup *ebpfMapsMockup) DeleteStackDeltaPage(host.FileID, uint64) error { + mockup.deleteStackDeltaPage++ + return nil +} + +func (mockup *ebpfMapsMockup) UpdatePidPageMappingInfo(pid libpf.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 + // a dummy entry. + return nil + } + if bias != mockup.expectedBias { + return fmt.Errorf("expected bias 0x%x for PID %d but got 0x%x", + mockup.expectedBias, pid, bias) + } + return nil +} + +func (mockup *ebpfMapsMockup) setExpectedBias(expected uint64) { + mockup.expectedBias = expected +} + +func (mockup *ebpfMapsMockup) DeletePidPageMappingInfo(_ libpf.PID, prefixes []lpm.Prefix) (int, + error) { + mockup.deletePidPageMappingCount += uint8(len(prefixes)) + return len(prefixes), nil +} + +func (mockup *ebpfMapsMockup) CollectMetrics() []metrics.Metric { return []metrics.Metric{} } +func (mockup *ebpfMapsMockup) SupportsGenericBatchOperations() bool { return false } +func (mockup *ebpfMapsMockup) SupportsLPMTrieBatchOperations() bool { return false } + +func TestInterpreterConvertTrace(t *testing.T) { + partialNativeFrameFileID := uint64(0xabcdbeef) + nativeFrameLineno := libpf.AddressOrLineno(0x1234) + pythonAndNativeTrace := &host.Trace{ + Frames: []host.Frame{{ + // This represents a native frame + File: host.FileID(partialNativeFrameFileID), + Lineno: nativeFrameLineno, + Type: libpf.NativeFrame, + }, { + File: host.FileID(42), + Lineno: libpf.AddressOrLineno(0x13e1bb8e), // same as runForeverTrace + Type: libpf.PythonFrame, + }}, + } + + tests := map[string]struct { + trace *host.Trace + expect *libpf.Trace + }{ + "Convert Trace": { + trace: pythonAndNativeTrace, + expect: getExpectedTrace(pythonAndNativeTrace, + []libpf.AddressOrLineno{0, 1}), + }, + } + + for name, testcase := range tests { + name := name + testcase := testcase + t.Run(name, func(t *testing.T) { + mapper := NewMapFileIDMapper() + for i := range testcase.trace.Frames { + mapper.Set(testcase.trace.Frames[i].File, testcase.expect.Files[i]) + } + + interpreters := make([]bool, config.MaxTracers) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // To test ConvertTrace we do not require all parts of processmanager. + manager, err := New(ctx, + interpreters, + 1*time.Second, + nil, + nil, + nil, + nil, + true) + if err != nil { + t.Fatalf("Failed to initialize new process manager: %v", 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) + } + }) + } +} + +// getExpectedTrace returns a new libpf trace that is based on the provided host trace, but +// with the linenos replaced by the provided values. This function is for generating an expected +// trace for tests below. +func getExpectedTrace(origTrace *host.Trace, linenos []libpf.AddressOrLineno) *libpf.Trace { + newTrace := &libpf.Trace{ + Hash: libpf.NewTraceHash(uint64(origTrace.Hash), uint64(origTrace.Hash)), + } + + for _, frame := range origTrace.Frames { + newTrace.Files = append(newTrace.Files, libpf.NewFileID(uint64(frame.File), 0)) + newTrace.FrameTypes = append(newTrace.FrameTypes, frame.Type) + if linenos == nil { + newTrace.Linenos = append(newTrace.Linenos, frame.Lineno) + } + } + if linenos != nil { + newTrace.Linenos = linenos + } + + return newTrace +} + +func TestNewMapping(t *testing.T) { + tests := map[string]struct { + // newMapping holds the arguments that are passed to NewMapping() in the test. + // For each mappingArgs{} a temporary file will be created. + newMapping []mappingArgs + // duplicate indicates if for each {}mappingArgs the generated dummy file + // should be loaded twice to simulate a duplicate loading. + duplicate bool + // expectedStackDeltas holds the number of stack deltas that are + // expected after loading all temporary files with the arguments from newMapping. + expectedStackDeltas int + }{ + "regular load": {newMapping: []mappingArgs{ + {pid: 1, vaddr: 0x10000, bias: 0x0000}, + {pid: 2, vaddr: 0x40000, bias: 0x2000}, + {pid: 3, vaddr: 0x60000, bias: 0x3000}, + {pid: 4, vaddr: 0x40000, bias: 0x4000}}, + expectedStackDeltas: 28}, + "duplicate load": {newMapping: []mappingArgs{ + {pid: 123, vaddr: 0x0F000, bias: 0x1000}, + {pid: 456, vaddr: 0x50000, bias: 0x4000}, + {pid: 789, vaddr: 0x40000, bias: 0}}, + duplicate: true, + expectedStackDeltas: 21}, + } + + cacheDir, err := os.MkdirTemp("", "*_cacheDir") + if err != nil { + t.Fatalf("Failed to create cache directory: %v", err) + } + defer os.RemoveAll(cacheDir) + + if err = config.SetConfiguration(&config.Config{ + ProjectID: 42, + CacheDirectory: cacheDir, + SecretToken: "secret"}); err != nil { + t.Fatalf("failed to set temporary config: %s", err) + } + + for name, testcase := range tests { + testcase := testcase + t.Run(name, func(t *testing.T) { + // The generated dummy files do not contain valid stack deltas, + // so we replace the stack delta provider. + dummyProvider := dummyStackDeltaProvider{} + ebpfMockup := &ebpfMapsMockup{} + + // For this test do not include interpreters. + noInterpreters := make([]bool, config.MaxTracers) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + manager, err := New(ctx, + noInterpreters, + 1*time.Second, + ebpfMockup, + NewMapFileIDMapper(), + nil, + &dummyProvider, + true) + if err != nil { + t.Fatalf("Failed to initialize new process manager: %v", err) + } + + // Replace the internal hooks for the tests. These hooks catch the + // updates of the eBPF maps and let us compare the results. + manager.metricsAddSlice = func(m []metrics.Metric) { + for id, value := range m { + t.Logf("Added +%d to metric %d\n", value, id) + } + } + + execs := generateDummyFiles(t, len(testcase.newMapping)) + defer func() { + for _, exe := range execs { + os.Remove(exe) + } + }() + + if testcase.duplicate { + execs = append(execs, execs...) + } + + // For the duplicate test case we have more test files than provided + // arguments to NewMapping(). modulo makes sure we don't exceed the + // index of the provided arguments to NewMapping(). + modulo := len(testcase.newMapping) + + // Simulate new memory mappings + for index, exec := range execs { + pid := testcase.newMapping[index%modulo].pid + vaddr := libpf.Address(testcase.newMapping[index%modulo].vaddr) + bias := testcase.newMapping[index%modulo].bias + ebpfMockup.setExpectedBias(bias) + + pr := newTestProcess(pid) + elfRef := pfelf.NewReference(exec, pr) + err := manager.handleNewMapping( + pr, &Mapping{ + FileID: host.FileID(index % modulo), + Vaddr: vaddr, + Bias: bias, + Length: 0x10, + }, elfRef) + elfRef.Close() + if err != nil { + t.Fatalf("Failed to add new mapping: %v", err) + } + } + + if len(ebpfMockup.stackDeltaMemory) != testcase.expectedStackDeltas { + t.Fatalf("Expected %d entries in big_stack_deltas but got %d", + testcase.expectedStackDeltas, + len(ebpfMockup.stackDeltaMemory)) + } + }) + } +} + +// populateManager fills the internal maps of the process manager with some dummy information. +func populateManager(t *testing.T, pm *ProcessManager) { + t.Helper() + + data := []struct { + pid libpf.PID + + mapping Mapping + }{ + { + pid: 1, + mapping: Mapping{ + FileID: host.FileID(127), + Vaddr: libpf.Address(0x1000), + Bias: 0x1000, + Length: 127, + }, + }, { + pid: 2, + mapping: Mapping{ + FileID: host.FileID(128), + Vaddr: libpf.Address(0x1000), + Bias: 0x1000, + Length: 128, + }, + }, { + pid: 2, + mapping: Mapping{ + FileID: host.FileID(129), + Vaddr: libpf.Address(0x1000), + Bias: 0x1000, + Length: 129, + }, + }, { + pid: 920, + mapping: Mapping{ + FileID: host.FileID(128), + Vaddr: libpf.Address(0x1000), + Bias: 0x1000, + Length: 128, + }, + }, { + pid: 921, + mapping: Mapping{ + FileID: host.FileID(129), + Vaddr: libpf.Address(0x2000), + Bias: 0x2000, + Length: 129, + }, + }, { + pid: 3, + mapping: Mapping{ + FileID: host.FileID(130), + Vaddr: libpf.Address(0x3000), + Bias: 0x3000, + Length: 130, + }, + }, { + pid: 3, + mapping: Mapping{ + FileID: host.FileID(131), + Vaddr: libpf.Address(0x4000), + Bias: 0x4000, + Length: 131, + }, + }, + } + + mockup := pm.ebpf.(*ebpfMapsMockup) + + for _, d := range data { + c := d + 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) + } + } +} + +func TestProcExit(t *testing.T) { + tests := map[string]struct { + // pid represents the ID of a process. + pid libpf.PID + // deletePidPageMappingCount reflects the number of times + // the deletePidPageMappingHook to update the eBPF map was called. + deletePidPageMappingCount uint8 + // deleteExeIDToIndicesCount reflects the number of times + // the deleteExeIDToIndicesHook to update the eBPF map was called. + deleteExeIDToIndicesCount uint8 + // deleteStackDeltaRangesCount reflects the number of times + // the deleteStackDeltaRangesHook to update the eBPF map was called. + deleteStackDeltaRangesCount uint8 + }{ + // unknown process simulates a test case where the process manager is + // informed about the process exit of a process it is not aware of. + "unknown process": {pid: 512}, + // process with single mapping simulates a test case where a process with a single + // memory mapping exits and this was the last mapping for the loaded executables. + "process with single mapping": {pid: 1, + deletePidPageMappingCount: 8, + deleteExeIDToIndicesCount: 1, + deleteStackDeltaRangesCount: 1}, + // process with multiple mapped mappings simulates a test case where a process with + // multiple memory mappings exits but the mappings are still referenced else where. + "process with multiple mapped mappings": {pid: 2, + deletePidPageMappingCount: 3, + deleteExeIDToIndicesCount: 0, + deleteStackDeltaRangesCount: 0}, + // process with multiple one-time mappings simulates a test case where a process with + // multiple one-time memory mappings exits and these mappings need to be removed. + "process with multiple one-time mappings": {pid: 3, + deletePidPageMappingCount: 6, + deleteExeIDToIndicesCount: 2, + deleteStackDeltaRangesCount: 2}, + } + + for name, testcase := range tests { + testcase := testcase + t.Run(name, func(t *testing.T) { + // The generated dummy files do not contain valid stack deltas, + // so we replace the stack delta provider. + dummyProvider := dummyStackDeltaProvider{} + ebpfMockup := &ebpfMapsMockup{} + + // For this test do not include interpreters. + noInterpreters := make([]bool, config.MaxTracers) + + ctx, cancel := context.WithCancel(context.Background()) + + manager, err := New(ctx, + noInterpreters, + 1*time.Second, + ebpfMockup, + NewMapFileIDMapper(), + nil, + &dummyProvider, + true) + if err != nil { + t.Fatalf("Failed to initialize new process manager: %v", err) + } + defer cancel() + + // Replace the internal hooks for the tests. These hooks catch the + // updates of the eBPF maps and let us compare the results. + manager.metricsAddSlice = func(m []metrics.Metric) { + for id, value := range m { + t.Logf("Added +%d to metric %d\n", value, id) + } + } + + 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) + } + }) + } +} diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go new file mode 100644 index 00000000..82103cd8 --- /dev/null +++ b/processmanager/processinfo.go @@ -0,0 +1,633 @@ +/* + * 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 processmanager + +// This file is the only place that should access pidToProcessInfo. +// The map is used to synchronize state between eBPF maps and process +// manager. The access needs to stay here so the interaction between +// these two components can be audited to be consistent. + +// The public functions in this file are restricted to be used from the +// HA/tracer and utils/coredump modules only. + +import ( + "context" + "errors" + "fmt" + "os" + "path" + "syscall" + "time" + + "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" + "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/proc" + eim "github.com/elastic/otel-profiling-agent/processmanager/execinfomanager" + "github.com/elastic/otel-profiling-agent/tpbase" + + log "github.com/sirupsen/logrus" +) + +// 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) { + if tsdInfo == nil { + return + } + + info, ok := pm.pidToProcessInfo[pid] + if !ok { + // This is guaranteed not to happen since assignTSDInfo is always called after + // pm.updatePidInformation - but to avoid a possible panic we just return here. + return + } else if info.tsdInfo != nil { + return + } + + info.tsdInfo = tsdInfo + + // Update the tsdInfo to interpreters that are already attached + for _, instance := range pm.interpreters[pid] { + if err := instance.UpdateTSDInfo(pm.ebpf, pid, *tsdInfo); err != nil { + log.Errorf("Failed to update PID %v TSDInfo: %v", + pid, err) + } + } +} + +// getTSDInfo retrieves the TSDInfo of given PID +// Caller must hold pm.mu read lock. +func (pm *ProcessManager) getTSDInfo(pid libpf.PID) *tpbase.TSDInfo { + if info, ok := pm.pidToProcessInfo[pid]; ok { + return info.tsdInfo + } + return nil +} + +// updatePidInformation updates pidToProcessInfo with the new information about +// vaddr, offset, fileID and length for the given pid. If we don't know about the pid yet, it also +// allocates the embedded map. If the mapping for pid at vaddr with requestedLength and fileID +// 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) { + info, ok := pm.pidToProcessInfo[pid] + if !ok { + // We don't have information for this pid, so we first need to + // allocate the embedded map for this process. + info = &processInfo{ + mappings: make(map[libpf.Address]Mapping), + tsdInfo: nil, + } + pm.pidToProcessInfo[pid] = info + + // Insert a dummy page into the eBPF map pid_page_to_mapping_info that provides the eBPF + // a quick way to check if we know something about this particular process. + if err := pm.ebpf.UpdatePidPageMappingInfo(pid, dummyPrefix, 0, 0); err != nil { + return false, fmt.Errorf( + "failed to update pid_page_to_mapping_info dummy entry for PID %d: %v", + pid, err) + } + pm.pidPageToMappingInfoSize++ + } else if mf, ok := info.mappings[m.Vaddr]; ok { + if *m == mf { + // We try to update our information about a particular mapping we already know about. + return true, nil + } + } + + info.mappings[m.Vaddr] = *m + + prefixes, err := lpm.CalculatePrefixList(uint64(m.Vaddr), uint64(m.Vaddr)+m.Length) + if err != nil { + return false, fmt.Errorf("failed to create LPM entries for PID %d: %v", pid, err) + } + numUpdates := uint64(0) + for _, prefix := range prefixes { + if err = pm.ebpf.UpdatePidPageMappingInfo(pid, prefix, uint64(m.FileID), + m.Bias); err != nil { + err = fmt.Errorf( + "failed to update pid_page_to_mapping_info (pid: %d, page: 0x%x/%d): %v", + pid, prefix.Key, prefix.Length, err) + break + } + numUpdates++ + } + + pm.pidPageToMappingInfoSize += numUpdates + + return false, err +} + +// 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 { + info, ok := pm.pidToProcessInfo[pid] + if !ok { + return fmt.Errorf("unknown PID %d: %w", pid, errUnknownPID) + } + + mapping, ok := info.mappings[addr] + if !ok { + return fmt.Errorf("unknown memory mapping for PID %d at 0x%x: %w", + pid, addr, errUnknownMapping) + } + + prefixes, err := lpm.CalculatePrefixList(uint64(addr), uint64(addr)+mapping.Length) + if err != nil { + return fmt.Errorf("failed to create LPM entries for PID %d: %v", pid, err) + } + + deleted, err := pm.ebpf.DeletePidPageMappingInfo(pid, prefixes) + if err != nil { + log.Errorf("Failed to delete mappings for PID %d: %v", pid, err) + } + + pm.pidPageToMappingInfoSize -= uint64(deleted) + delete(info.mappings, addr) + + return pm.eim.RemoveOrDecRef(mapping.FileID) +} + +// assignInterpreter will update the interpreters maps with given interpreter.Instance. +func (pm *ProcessManager) assignInterpreter(pid libpf.PID, key libpf.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][key] = instance +} + +// handleNewInterpreter is called to process new executable memory mappings. It uses the +// process manager to attach to the process/memory mapping if it is discovered that the +// memory mapping corresponds with an interpreter. +// +// It is important to note that this function may spawn a new goroutine in order to retry +// attaching to the interpreter, if the first attach attempt fails. In this case, `nil` will still +// be returned and thus a `nil` return value does not mean the attach was successful. It means +// that the attach was successful OR a retry is underway. +// +// The caller is responsible to hold the ProcessManager lock to avoid race conditions. +func (pm *ProcessManager) handleNewInterpreter(pr process.Process, m *Mapping, + ei *eim.ExecutableInfo) error { + // The same interpreter can be found multiple times under various different + // circumstances. Check if this is already handled. + pid := pr.PID() + key := m.GetOnDiskFileIdentifier() + if _, ok := pm.interpreters[pid]; ok { + if _, ok := pm.interpreters[pid][key]; ok { + return nil + } + } + // Slow path: Interpreter detection or attachment needed + instance, err := ei.Data.Attach(pm.ebpf, pid, libpf.Address(m.Bias), pr.GetRemoteMemory()) + if err != nil { + return fmt.Errorf("failed to attach to %v in PID %v: %w", + ei.Data, pid, err) + } + + log.Debugf("Attached to %v interpreter in PID %v", ei.Data, pid) + pm.assignInterpreter(pid, key, instance) + + if tsdInfo := pm.getTSDInfo(pid); tsdInfo != nil { + err = instance.UpdateTSDInfo(pm.ebpf, pid, *tsdInfo) + if err != nil { + log.Errorf("Failed to update PID %v TSDInfo: %v", pid, err) + } + } + + return nil +} + +// handleNewMapping processes new file backed mappings +func (pm *ProcessManager) handleNewMapping(pr process.Process, m *Mapping, + elfRef *pfelf.Reference) error { + // Resolve executable info first + ei, err := pm.eim.AddOrIncRef(m.FileID, elfRef) + if err != nil { + return err + } + + pid := pr.PID() + + // We intentionally don't take the lock immediately when entering this function and instead + // rely on EIM's internal locking for the `AddOrIncRef` call. The reasoning here is that + // the `AddOrIncRef` call can take a while, and we don't want to block the whole PM for that. + pm.mu.Lock() + defer pm.mu.Unlock() + + // Update the eBPF maps with information about this mapping. + _, err = pm.updatePidInformation(pid, m) + if err != nil { + return err + } + + pm.assignTSDInfo(pid, ei.TSDInfo) + + if ei.Data != nil { + return pm.handleNewInterpreter(pr, m, &ei) + } + + return nil +} + +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() + + if info, ok := pm.elfInfoCache.Get(key); ok && info.lastModified == lastModified { + // Cached data ok + pm.elfInfoCacheHit.Add(1) + return info + } + + // Slow path, calculate all the data and update cache + pm.elfInfoCacheMiss.Add(1) + + info := elfInfo{ + lastModified: lastModified, + } + + var fileID libpf.FileID + ef, err := elfRef.GetELF() + if err == nil { + fileID, err = pr.CalculateMappingFileID(mapping) + } + if err != nil { + info.err = err + // It is possible that the process has exited, and the mapping + // file cannot be opened. Do not cache these errors. + if !errors.Is(err, os.ErrNotExist) { + // Cache the other errors: not an ELF, ELF corrupt, etc. + // to reduce opening it again and again. + pm.elfInfoCache.Add(key, info) + } + return info + } + + hostFileID := host.CalculateKernelFileID(fileID) + info.fileID = hostFileID + info.addressMapper = ef.GetAddressMapper() + if mapping.IsVDSO() { + info.err = pm.insertSynthStackDeltas(hostFileID, ef) + } + // Do not cache the entry if synthetic stack delta loading failed, + // so next encounter of the VDSO will retry loading them. + if info.err == nil { + pm.elfInfoCache.Add(key, info) + } + pm.FileIDMapper.Set(hostFileID, fileID) + + baseName := path.Base(mapping.Path) + if baseName == "/" { + // There are circumstances where there is no filename. + // E.g. kernel module 'bpfilter_umh' before Linux 5.9-rc1 uses + // fork_usermode_blob() and launches process with a blob without + // filename mapped in as the executable. + baseName = "" + } + + buildID, _ := ef.GetBuildID() + pm.reporter.ExecutableMetadata(context.TODO(), fileID, baseName, buildID) + + return info +} + +// processNewExecMapping is the logic to add a new process.Mapping to processmanager. +func (pm *ProcessManager) processNewExecMapping(pr process.Process, mapping *process.Mapping) { + // Filter uninteresting mappings + if mapping.Inode == 0 && !mapping.IsVDSO() { + return + } + + // Create a Reference so we don't need to open the ELF multiple times + elfRef := pfelf.NewReference(mapping.Path, pr) + defer elfRef.Close() + + info := pm.getELFInfo(pr, mapping, elfRef) + if info.err != nil { + // Unable to get the information. Most likely cause is that the + // process has exited already and the mapping file is unavailable + // or it is not an ELF file. Ignore these errors silently. + if !errors.Is(info.err, os.ErrNotExist) && !errors.Is(info.err, pfelf.ErrNotELF) { + log.Debugf("Failed to get ELF info for PID %d file %v: %v", + pr.PID(), mapping.Path, info.err) + } + return + } + + // Get the virtual addresses for this mapping + elfSpaceVA, ok := info.addressMapper.FileOffsetToVirtualAddress(mapping.FileOffset) + if !ok { + log.Debugf("Failed to map file offset of PID %d, file %s, offset %d", + pr.PID(), mapping.Path, mapping.FileOffset) + return + } + + if err := pm.handleNewMapping(pr, + &Mapping{ + FileID: info.fileID, + Vaddr: libpf.Address(mapping.Vaddr), + Bias: mapping.Vaddr - elfSpaceVA, + Length: mapping.Length, + Device: mapping.Device, + 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) + } +} + +// 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]) { + pm.mu.Lock() + defer pm.mu.Unlock() + + for _, addr := range mappings { + if err := pm.deletePIDAddress(pid, addr); err != nil { + log.Debugf("Failed to handle native unmapping of 0x%x in PID %d: %v", + addr, pid, err) + } + } + + if !pm.interpreterTracerEnabled { + return + } + + if _, ok := pm.interpreters[pid]; !ok { + log.Debugf("ProcessManager doesn't know about PID %d", pid) + return + } + + for key, instance := range pm.interpreters[pid] { + if _, ok := interpretersValid[key]; ok { + continue + } + if err := instance.Detach(pm.ebpf, pid); err != nil { + log.Errorf("Failed to unload interpreter for PID %d: %v", + pid, err) + } + delete(pm.interpreters[pid], key) + } + + if len(pm.interpreters[pid]) == 0 { + // There are no longer any mapped interpreters in the process, therefore we can + // remove the entry. + delete(pm.interpreters, pid) + } +} + +// synchronizeMappings synchronizes executable mappings for the given PID. +// This method will be called when a PID is first encountered or when the eBPF +// code encounters an address in an executable mapping that HA has no information +// on. Therefore, executable mapping synchronization takes place lazily on-demand, +// and map/unmap operations are not precisely tracked (reduce processing load). +// This means that at any point, we may have cached stale (or miss) executable +// mappings. The expectation is that stale mappings will disappear and new +// mappings cached at the next synchronization triggered by process exit or +// unknown address encountered. +// +// TODO: Periodic synchronization of mappings for every tracked PID. +func (pm *ProcessManager) synchronizeMappings(pr process.Process, + mappings []process.Mapping) bool { + newProcess := true + pid := pr.PID() + mpAdd := make(map[libpf.Address]*process.Mapping, len(mappings)) + mpRemove := make([]libpf.Address, 0) + + interpretersValid := make(libpf.Set[libpf.OnDiskFileIdentifier]) + for idx := range mappings { + m := &mappings[idx] + if !m.IsExecutable() || m.IsAnonymous() { + continue + } + mpAdd[libpf.Address(m.Vaddr)] = m + key := m.GetOnDiskFileIdentifier() + interpretersValid[key] = libpf.Void{} + } + + // Generate the list of added and removed mappings. + pm.mu.RLock() + if info, ok := pm.pidToProcessInfo[pid]; ok { + // Iterate over cached executable mappings, if any, and collect mappings + // that have changed so that they are later batch-removed. + for addr, existingMapping := range info.mappings { + if newMapping, ok := mpAdd[addr]; ok { + // Check the relevant fields to see if it's still the same + if newMapping.Device == existingMapping.Device && + newMapping.Inode == existingMapping.Inode && + newMapping.FileOffset == existingMapping.FileOffset && + newMapping.Length == existingMapping.Length { + // Mapping hasn't changed, remove from the new set + delete(mpAdd, addr) + continue + } + } + // Mapping has changed + mpRemove = append(mpRemove, addr) + } + newProcess = false + } + pm.mu.RUnlock() + + // First, remove mappings that have changed + pm.processRemovedMappings(pid, mpRemove, interpretersValid) + + // Add the new ELF mappings + for _, mapping := range mpAdd { + // Output memory usage in debug builds. + memorydebug.DebugLogMemoryUsage() + pm.processNewExecMapping(pr, mapping) + } + + // Update interpreter plugins about the changed mappings + if pm.interpreterTracerEnabled { + pm.mu.Lock() + for _, instance := range pm.interpreters[pid] { + err := instance.SynchronizeMappings(pm.ebpf, pm.reporter, pr, mappings) + if err != nil { + if alive, _ := proc.IsPIDLive(pid); alive { + log.Errorf("Failed to handle new anonymous mapping for PID %d: %v", pid, err) + } else { + log.Debugf("Failed to handle new anonymous mapping for PID %d: process exited", + pid) + } + } + } + pm.mu.Unlock() + } + + if len(mpAdd) > 0 || len(mpRemove) > 0 { + log.Debugf("Added %v mappings, removed %v mappings for PID: %v", + len(mpAdd), len(mpRemove), pid) + } + return newProcess +} + +// ProcessPIDExit informs the ProcessManager that a process exited and no longer will be scheduled +// for processing. It also schedules immediate symbolization if the exited PID needs it. exitKTime +// 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 { + log.Debugf("- PID: %v", pid) + defer pm.ebpf.RemoveReportedPID(pid) + + pm.mu.Lock() + defer pm.mu.Unlock() + + symbolize := false + exitKTime := libpf.GetKTime() + if pm.interpreterTracerEnabled { + if len(pm.interpreters[pid]) > 0 { + pm.exitEvents[pid] = exitKTime + symbolize = true + } + } + + info, ok := pm.pidToProcessInfo[pid] + if !ok { + log.Debugf("Skip process exit handling for unknown PID %d", pid) + return symbolize + } + + // Delete all entries we have for this particular PID from pid_page_to_mapping_info. + deleted, err := pm.ebpf.DeletePidPageMappingInfo(pid, []lpm.Prefix{dummyPrefix}) + if err != nil { + log.Errorf("Failed to delete dummy prefix for PID %d: %v", + pid, err) + } + pm.pidPageToMappingInfoSize -= uint64(deleted) + + for address := range info.mappings { + if err := pm.deletePIDAddress(pid, address); err != nil { + log.Errorf("Failed to delete address 0x%x for PID %d: %v", + address, pid, err) + } + } + delete(pm.pidToProcessInfo, pid) + + return symbolize +} + +func (pm *ProcessManager) SynchronizeProcess(pr process.Process) { + pid := pr.PID() + log.Debugf("= PID: %v", pid) + + pm.mappingStats.numProcAttempts.Add(1) + start := time.Now() + mappings, err := pr.GetMappings() + elapsed := time.Since(start) + + if err != nil { + if os.IsPermission(err) { + // Ignore the synchronization completely in case of permission + // error. This implies the process is still alive, but we cannot + // inspect it. Exiting here keeps the PID in the eBPF maps so + // we avoid a notification flood to resynchronize. + pm.mappingStats.errProcPerm.Add(1) + return + } + + // All other errors imply that the process has exited. + // Clean up, and notify eBPF. + pm.ProcessPIDExit(pid) + if os.IsNotExist(err) { + // Since listing /proc and opening files in there later is inherently racy, + // we expect to lose the race sometimes and thus expect to hit os.IsNotExist. + pm.mappingStats.errProcNotExist.Add(1) + } else if e, ok := err.(*os.PathError); ok && e.Err == syscall.ESRCH { + // If the process exits while reading its /proc/$PID/maps, the kernel will + // return ESRCH. Handle it as if the process did not exists. + pm.mappingStats.errProcESRCH.Add(1) + } + return + } + if len(mappings) == 0 { + // Valid process without any (executable) mappings. All cases are + // handled as process exit. Possible causes and reasoning: + // 1. It is a kernel worker process. The eBPF does not send events from these, + // but we can see kernel threads here during startup when tracer walks + // /proc and tries to synchronize all PIDs it sees. + // The PID should not exist anywhere, but we can still double check and + // make sure the PID is not tracked. + // 2. It is a normal process executing, but we just sampled it when the kernel + // execve() is rebuilding the mappings and nothing is currently mapped. + // In this case we can handle it as process exit because everything about + // the process is changing: all mappings, comm, etc. If execve fails, we + // reaped it early. If execve succeeds, we will get new synchronization + // request soon, and handle it as a new process event. + pm.ProcessPIDExit(pid) + return + } + + libpf.AtomicUpdateMaxUint32(&pm.mappingStats.maxProcParseUsec, uint32(elapsed.Microseconds())) + pm.mappingStats.totalProcParseUsec.Add(uint32(elapsed.Microseconds())) + + if pm.synchronizeMappings(pr, mappings) { + log.Debugf("+ PID: %v", pid) + // TODO: Fine-grained reported_pids handling (evaluate per-PID mapping + // synchronization based on per-PID state such as time since last + // synchronization). Currently we only remove a PID from reported_pids + // if it's a new process and on process exit. This limits + // the frequency of PID mapping synchronizations to PID lifetime in + // reported_pids (which is dictated by REPORTED_PIDS_TIMEOUT in eBPF). + + // We're immediately removing a new PID from reported_pids, to cover + // corner cases where processes load on startup in quick-succession + // additional code (e.g. plugins, Asterisk). + // Also see: Unified PID Events design doc + pm.ebpf.RemoveReportedPID(pid) + } +} + +// CleanupPIDs executes a periodic synchronization of pidToProcessInfo table with system processes. +// NOTE: Exported only for tracer/. +func (pm *ProcessManager) CleanupPIDs() { + deadPids := make([]libpf.PID, 0, 16) + + pm.mu.RLock() + for pid := range pm.pidToProcessInfo { + if live, _ := proc.IsPIDLive(pid); !live { + deadPids = append(deadPids, pid) + } + } + pm.mu.RUnlock() + + for _, pid := range deadPids { + pm.ProcessPIDExit(pid) + } + + if len(deadPids) > 0 { + log.Debugf("Cleaned up %d dead PIDs", len(deadPids)) + } +} diff --git a/processmanager/synthdeltas_arm64.go b/processmanager/synthdeltas_arm64.go new file mode 100644 index 00000000..26416e01 --- /dev/null +++ b/processmanager/synthdeltas_arm64.go @@ -0,0 +1,48 @@ +//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. + * See the file "LICENSE" for details. + */ + +package processmanager + +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" +) + +// createVDSOSyntheticRecord creates a generated stack-delta record spanning the entire vDSO binary, +// requesting LR based unwinding. On ARM64, the vDSO currently lacks a proper `.eh_frame` section, +// so we construct it here instead. +// Currently, this assumes that most calls work with the LR unwinding. Special handling +// is added for the signal frame return handler stub which uses signal unwinding. +func createVDSOSyntheticRecord(ef *pfelf.File) sdtypes.IntervalData { + useLR := sdtypes.UnwindInfo{ + Opcode: sdtypes.UnwindOpcodeBaseSP, + FPOpcode: sdtypes.UnwindOpcodeBaseLR, + } + + deltas := sdtypes.StackDeltaArray{} + 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}) + } + return sdtypes.IntervalData{Deltas: deltas} +} + +// insertSynthStackDeltas adds synthetic stack-deltas to the given SDMM. On ARM64, this is +// currently only used for emulating proper unwinding info of the vDSO. +func (pm *ProcessManager) insertSynthStackDeltas(fileID host.FileID, ef *pfelf.File) error { + deltas := createVDSOSyntheticRecord(ef) + if err := pm.AddSynthIntervalData(fileID, deltas); err != nil { + return fmt.Errorf("failed to add synthetic deltas: %w", err) + } + return nil +} diff --git a/processmanager/synthdeltas_other.go b/processmanager/synthdeltas_other.go new file mode 100644 index 00000000..974e0eb0 --- /dev/null +++ b/processmanager/synthdeltas_other.go @@ -0,0 +1,20 @@ +//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. + * See the file "LICENSE" for details. + */ + +package processmanager + +import ( + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" +) + +// insertSynthStackDeltas adds synthetic stack-deltas to the given SDMM. On non-ARM64, this is +// currently unused. +func (pm *ProcessManager) insertSynthStackDeltas(_ host.FileID, _ *pfelf.File) error { + return nil +} diff --git a/processmanager/types.go b/processmanager/types.go new file mode 100644 index 00000000..5ba7420e --- /dev/null +++ b/processmanager/types.go @@ -0,0 +1,151 @@ +/* + * 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 processmanager + +import ( + "sync" + "sync/atomic" + + lru "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/pfelf" + "github.com/elastic/otel-profiling-agent/metrics" + pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" + eim "github.com/elastic/otel-profiling-agent/processmanager/execinfomanager" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/tpbase" +) + +// elfInfo contains cached data from an executable needed for processing mappings. +// A negative cache entry may also be recorded with err set to indicate permanent +// error. This avoids inspection of non-ELF or corrupted files again and again. +type elfInfo struct { + err error + lastModified int64 + fileID host.FileID + addressMapper pfelf.AddressMapper +} + +// ProcessManager is responsible for managing the events happening throughout the lifespan of a +// process. +type ProcessManager struct { + // A mutex to synchronize access to internal data within this struct. + mu sync.RWMutex + + // interpreterTracerEnabled indicates if at last one non-native tracer is loaded. + interpreterTracerEnabled bool + + // eim stores per executable (file ID) information. + eim *eim.ExecutableInfoManager + + // interpreters records the interpreter.Instance interface which contains hooks for + // 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 + + // pidToProcessInfo keeps track of the executable memory mappings in addressSpace + // for each pid. + pidToProcessInfo map[libpf.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 + + // ebpf contains the interface to manipulate ebpf maps + ebpf pmebpf.EbpfHandler + + // FileIDMapper provides a cache that implements the FileIDMapper interface. The tracer writes + // the 64-bit to 128-bit file ID mapping to the cache, as this is where the two values are + // created. The attached interpreters read from the cache when converting traces prior to + // sending to the collection agent. The cache resides in this package instead of the ebpf + // package to prevent circular imports. + FileIDMapper FileIDMapper + + // elfInfoCacheHit + elfInfoCacheHit atomic.Uint64 + elfInfoCacheMiss atomic.Uint64 + + // mappingStats are statistics for parsing process mappings + mappingStats struct { + errProcNotExist atomic.Uint32 + errProcESRCH atomic.Uint32 + errProcPerm atomic.Uint32 + numProcAttempts atomic.Uint32 + maxProcParseUsec atomic.Uint32 + totalProcParseUsec atomic.Uint32 + } + + // 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] + + // reporter is the interface to report symbolization information + reporter reporter.SymbolReporter + + // Reporting function which is used to report information to our backend. + metricsAddSlice func([]metrics.Metric) + + // pidPageToMappingInfoSize reflects the current size of the eBPF hash map + // pid_page_to_mapping_info. + pidPageToMappingInfoSize uint64 + + // filterErrorFrames determines whether error frames are dropped by `ConvertTrace`. + filterErrorFrames bool +} + +// Mapping represents an executable memory mapping of a process. +type Mapping struct { + // FileID represents the host-wide unique identifier of the mapped file. + FileID host.FileID + + // Vaddr represents the starting virtual address of the mapping. + Vaddr libpf.Address + + // Bias is the offset between the ELF on-disk virtual address space and the + // virtual address where it is actually mapped in the process. Thus it is the + // virtual address bias or "ASLR offset". It serves as a translation offset + // from the process VA space into the VA space of the ELF file. It's calculated as + // `bias = vaddr_in_proc - vaddr_in_elf`. + // Adding the bias to a VA in ELF space translates it into process space. + Bias uint64 + + // Length represents the memory size of the mapping. + Length uint64 + + // Device number of the backing file + Device uint64 + + // Inode number of the backing file + Inode uint64 + + // File offset of the backing file + FileOffset uint64 +} + +// GetOnDiskFileIdentifier returns the OnDiskFileIdentifier for the mapping +func (m *Mapping) GetOnDiskFileIdentifier() libpf.OnDiskFileIdentifier { + return libpf.OnDiskFileIdentifier{ + DeviceID: m.Device, + InodeNum: m.Inode, + } +} + +// addressSpace represents the address space of a process. It maps the known start addresses +// of executable mappings to the corresponding mappedFile information. +type addressSpace map[libpf.Address]Mapping + +// processInfo contains information about the executable mappings +// and Thread Specific Data of a process. +type processInfo struct { + // executable mappings + mappings addressSpace + // C-library Thread Specific Data information + tsdInfo *tpbase.TSDInfo +} diff --git a/proto/.gitignore b/proto/.gitignore new file mode 100644 index 00000000..a245b848 --- /dev/null +++ b/proto/.gitignore @@ -0,0 +1 @@ +experiments diff --git a/proto/buildpb.sh b/proto/buildpb.sh new file mode 100755 index 00000000..0806cffc --- /dev/null +++ b/proto/buildpb.sh @@ -0,0 +1,28 @@ +#!/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 new file mode 100644 index 00000000..1e0258e5 --- /dev/null +++ b/proto/opentelemetry/proto/collector/profiles/v1/profiles_service.proto @@ -0,0 +1,83 @@ +// 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 new file mode 100644 index 00000000..42bf3a3f --- /dev/null +++ b/proto/opentelemetry/proto/common/v1/common.proto @@ -0,0 +1,82 @@ +// 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 new file mode 100644 index 00000000..1d2b5710 --- /dev/null +++ b/proto/opentelemetry/proto/profiles/v1/alternatives/pprofextended/pprofextended.proto @@ -0,0 +1,326 @@ +// 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 new file mode 100644 index 00000000..d73699ae --- /dev/null +++ b/proto/opentelemetry/proto/profiles/v1/profiles.proto @@ -0,0 +1,153 @@ +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 new file mode 100644 index 00000000..bc397dde --- /dev/null +++ b/proto/opentelemetry/proto/resource/v1/resource.proto @@ -0,0 +1,38 @@ +// 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/reporter/fifo.go b/reporter/fifo.go new file mode 100644 index 00000000..60d7cdc6 --- /dev/null +++ b/reporter/fifo.go @@ -0,0 +1,125 @@ +/* + * 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 ( + "fmt" + "sync" + + 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 { + sync.Mutex + + // data holds the actual data. + data []T + + // emptyT is variable of type T used for nullifying entries in data[]. + emptyT T + + // name holds a string to uniquely identify the ring buffer in log messages. + name string + + // size is the maximum number of entries in the ring buffer. + size uint32 + + // readPos holds the position of the first element to be read in the data array. + readPos uint32 + + // writePos holds the position where the next element should be + // placed in the data array. + writePos uint32 + + // count holds a count of how many entries are in the array. + count uint32 + + // overwriteCount holds a count of the number of overwritten entries since the last metric + // report interval. + overwriteCount uint32 +} + +func (q *fifoRingBuffer[T]) initFifo(size uint32, name string) error { + if size == 0 { + return fmt.Errorf("unsupported size of fifo: %d", size) + } + q.Lock() + defer q.Unlock() + q.size = size + q.data = make([]T, size) + q.readPos = 0 + q.writePos = 0 + q.count = 0 + q.overwriteCount = 0 + q.name = name + return nil +} + +// 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 { + // Should never happen + panic(err) + } +} + +// append adds element v to the fifoRingBuffer. it overwrites existing elements if there is no +// space left. +func (q *fifoRingBuffer[T]) append(v T) { + q.Lock() + defer q.Unlock() + + q.data[q.writePos] = v + q.writePos++ + + if q.writePos == q.size { + q.writePos = 0 + } + + if q.count < q.size { + q.count++ + if q.count == q.size { + log.Warnf("About to start overwriting elements in buffer for %s", + q.name) + } + } else { + q.overwriteCount++ + q.readPos = q.writePos + } +} + +// readAll returns all elements from the fifoRingBuffer. +func (q *fifoRingBuffer[T]) readAll() []T { + q.Lock() + defer q.Unlock() + + data := make([]T, q.count) + readPos := q.readPos + + for i := uint32(0); i < q.count; i++ { + pos := (i + readPos) % q.size + data[i] = q.data[pos] + // Allow for element to be GCed + q.data[pos] = q.emptyT + } + + q.readPos = q.writePos + q.count = 0 + + return data +} + +func (q *fifoRingBuffer[T]) getOverwriteCount() uint32 { + q.Lock() + defer q.Unlock() + + count := q.overwriteCount + q.overwriteCount = 0 + return count +} diff --git a/reporter/fifo_test.go b/reporter/fifo_test.go new file mode 100644 index 00000000..b9f560e9 --- /dev/null +++ b/reporter/fifo_test.go @@ -0,0 +1,141 @@ +/* + * 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/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" +) + +func TestFifo(t *testing.T) { + var integers []int + integers = append(integers, 1, 2, 3, 4, 5) + + var integersShared []int + integersShared = append(integersShared, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) + + var retIntegers []int + retIntegers = append(retIntegers, 3, 4, 5) + + 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) + } + + // nolint:lll + tests := map[string]struct { + // size defines the size of the fifo. + size uint32 + // data will be written to and extracted from the fifo. + data []int + // returned reflects the data that is expected from the fifo + // after writing to it. + returned []int + // the number of overwrites that occurred + overwriteCount uint32 + // err indicates if an error is expected for this testcase. + err bool + // sharedFifo indicates if a shared fifo should be used. + // If false, a new fifo is used, specific to the testcase. + sharedFifo bool + // parallel indicates if parallelism should be enabled for this testcase. + parallel bool + }{ + // This testcase simulates a fifo with an invalid size of 0. + "Invalid size": {size: 0, err: true, parallel: true}, + // This testcase simulates a case where the numbers of elements + // written to the fifo represents the size of the fifo. + "Full Fifo": {size: 5, data: integers, returned: integers, overwriteCount: 0, parallel: true}, + // This testcase simulates a case where the number of elements + // written to the fifo exceed the size of the fifo. + "Fifo overflow": {size: 3, data: integers, returned: retIntegers, overwriteCount: 2, parallel: true}, + // This testcase simulates a case where only a few elements are + // written to the fifo and don't exceed the size of the fifo. + "Partial full": {size: 15, data: integers, returned: integers, overwriteCount: 0, parallel: true}, + + // The following test cases share the same fifo + + // This testcase simulates a case where the numbers of elements + // written to the fifo represents the size of the fifo. + "Shared Full Fifo": {data: integers, returned: integers, overwriteCount: 0, sharedFifo: true}, + // This testcase simulates a case where the number of elements + // written to the fifo exceed the size of the fifo. + "Shared Fifo overflow": {data: integersShared, returned: retIntegersShared, overwriteCount: 7, sharedFifo: true}, + } + + for name, testcase := range tests { + name := name + testcase := testcase + var fifo *fifoRingBuffer[int] + + t.Run(name, func(t *testing.T) { + if testcase.parallel { + t.Parallel() + } + + 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) + } + } + + empty := fifo.readAll() + if len(empty) != 0 { + t.Fatalf("Nothing was added to fifo but fifo returned %d elements", len(empty)) + } + + for _, v := range testcase.data { + fifo.append(v) + } + + 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) + } + }) + } +} + +func TestFifo_isWritableWhenZeroed(t *testing.T) { + fifo := &fifoRingBuffer[int]{} + assert.Nil(t, fifo.initFifo(1, t.Name())) + fifo.zeroFifo() + assert.NotPanics(t, func() { + fifo.append(123) + }) +} diff --git a/reporter/helper.go b/reporter/helper.go new file mode 100644 index 00000000..53a589f4 --- /dev/null +++ b/reporter/helper.go @@ -0,0 +1,107 @@ +/* + * 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 new file mode 100644 index 00000000..18281201 --- /dev/null +++ b/reporter/iface.go @@ -0,0 +1,81 @@ +/* + * 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" + "time" + + "github.com/elastic/otel-profiling-agent/config" + "github.com/elastic/otel-profiling-agent/libpf" +) + +// 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 + SymbolReporter + HostMetadataReporter + MetricsReporter + + // Stop triggers a graceful shutdown of the reporter. + Stop() + // GetMetrics returns the reporter internal metrics. + GetMetrics() Metrics +} + +type TraceReporter interface { + // ReportFramesForTrace accepts a trace with the corresponding frames + // and caches this information before a periodic reporting to the backend. + ReportFramesForTrace(trace *libpf.Trace) + + // 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) +} + +type SymbolReporter interface { + // ReportFallbackSymbol enqueues a fallback symbol for reporting, for a given frame. + ReportFallbackSymbol(frameID libpf.FrameID, symbol string) + + // ExecutableMetadata accepts a fileID with the corresponding filename + // and caches this information before a periodic reporting to the backend. + ExecutableMetadata(ctx context.Context, fileID libpf.FileID, fileName, buildID string) + + // 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) +} + +type HostMetadataReporter interface { + // ReportHostMetadata enqueues host metadata for sending (to the collection agent). + ReportHostMetadata(metadataMap map[string]string) + + // ReportHostMetadataBlocking sends host metadata to the collection agent. + ReportHostMetadataBlocking(ctx context.Context, metadataMap map[string]string, + maxRetries int, waitRetry time.Duration) error +} + +type MetricsReporter interface { + // ReportMetrics accepts an id with a corresponding value and caches this + // information before a periodic reporting to the backend. + ReportMetrics(timestamp uint32, ids []uint32, values []int64) +} diff --git a/reporter/metrics.go b/reporter/metrics.go new file mode 100644 index 00000000..c9d1ec7c --- /dev/null +++ b/reporter/metrics.go @@ -0,0 +1,212 @@ +/* + * 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" + "fmt" + "sync/atomic" + + "google.golang.org/grpc/stats" + + "github.com/elastic/otel-profiling-agent/libpf/xsync" +) + +type statsHandlerImpl struct { + // Total number of uncompressed bytes in/out + numRPCBytesOut atomic.Int64 + numRPCBytesIn atomic.Int64 + + // Total number of on-the-wire (post-compression) bytes in/out + numWireBytesOut atomic.Int64 + numWireBytesIn atomic.Int64 + + // These two maps aggregate total in/out byte counts under each RPC method name + rpcBytesOut xsync.RWMutex[map[string]uint64] + rpcBytesIn xsync.RWMutex[map[string]uint64] + + // These two maps aggregate total in/out byte counts under each RPC method name + wireBytesOut xsync.RWMutex[map[string]uint64] + wireBytesIn xsync.RWMutex[map[string]uint64] +} + +// Make sure that the handler implements stats.Handler. +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{ + rpcBytesOut: xsync.NewRWMutex(map[string]uint64{}), + rpcBytesIn: xsync.NewRWMutex(map[string]uint64{}), + wireBytesOut: xsync.NewRWMutex(map[string]uint64{}), + wireBytesIn: xsync.NewRWMutex(map[string]uint64{}), + } +} + +// TagRPC implements the stats.Handler interface. +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 { + return ctx +} + +// HandleConn implements the stats.Handler interface. +func (sh *statsHandlerImpl) HandleConn(context.Context, stats.ConnStats) { +} + +func rpcMethodFromContext(ctx context.Context) (string, error) { + tagInfo, ok := ctx.Value(&keyRPCTagInfo).(*stats.RPCTagInfo) + if !ok { + return "", fmt.Errorf("missing context key: %v", keyRPCTagInfo) + } + return tagInfo.FullMethodName, nil +} + +// HandleRPC implements the stats.Handler interface. +func (sh *statsHandlerImpl) HandleRPC(ctx context.Context, s stats.RPCStats) { + var wireBytesIn, wireBytesOut, rpcBytesIn, rpcBytesOut int64 + + switch s := s.(type) { + case *stats.InPayload: + // WireLength is the length of data on wire (compressed, signed, encrypted, + // with gRPC framing). + wireBytesIn = int64(s.WireLength) + // Length is the uncompressed payload data + rpcBytesIn = int64(s.Length) + case *stats.OutPayload: + wireBytesOut = int64(s.WireLength) + rpcBytesOut = int64(s.Length) + default: + return + } + + method, err := rpcMethodFromContext(ctx) + if err != nil { + // If this happens, it's a bug and we should visibly exit (context must contain tag) + panic(err) + } + + if wireBytesIn != 0 { + sh.numWireBytesIn.Add(wireBytesIn) + sh.numRPCBytesIn.Add(rpcBytesIn) + wireIn := sh.wireBytesIn.WLock() + rpcIn := sh.rpcBytesIn.WLock() + defer sh.wireBytesIn.WUnlock(&wireIn) + defer sh.rpcBytesIn.WUnlock(&rpcIn) + (*wireIn)[method] += uint64(wireBytesIn) + (*rpcIn)[method] += uint64(rpcBytesIn) + } + + if wireBytesOut != 0 { + sh.numWireBytesOut.Add(wireBytesOut) + wireOut := sh.wireBytesOut.WLock() + rpcOut := sh.rpcBytesOut.WLock() + defer sh.wireBytesOut.WUnlock(&wireOut) + defer sh.rpcBytesOut.WUnlock(&rpcOut) + (*wireOut)[method] += uint64(wireBytesOut) + (*rpcOut)[method] += uint64(rpcBytesOut) + } +} + +func (sh *statsHandlerImpl) getWireBytesOut() int64 { + return sh.numWireBytesOut.Swap(0) +} + +func (sh *statsHandlerImpl) getWireBytesIn() int64 { + return sh.numWireBytesIn.Swap(0) +} + +func (sh *statsHandlerImpl) getRPCBytesOut() int64 { + return sh.numRPCBytesOut.Swap(0) +} + +func (sh *statsHandlerImpl) getRPCBytesIn() int64 { + return sh.numRPCBytesIn.Swap(0) +} + +// nolint:unused +func (sh *statsHandlerImpl) getMethodRPCBytesOut() map[string]uint64 { + rpcOut := sh.rpcBytesOut.RLock() + defer sh.rpcBytesOut.RUnlock(&rpcOut) + res := make(map[string]uint64, len(*rpcOut)) + for k, v := range *rpcOut { + res[k] = v + } + return res +} + +// nolint:unused +func (sh *statsHandlerImpl) getMethodRPCBytesIn() map[string]uint64 { + rpcIn := sh.rpcBytesIn.RLock() + defer sh.rpcBytesIn.RUnlock(&rpcIn) + res := make(map[string]uint64, len(*rpcIn)) + for k, v := range *rpcIn { + res[k] = v + } + return res +} + +// nolint:unused +func (sh *statsHandlerImpl) getMethodWireBytesOut() map[string]uint64 { + wireOut := sh.wireBytesOut.RLock() + defer sh.wireBytesOut.RUnlock(&wireOut) + res := make(map[string]uint64, len(*wireOut)) + for k, v := range *wireOut { + res[k] = v + } + return res +} + +// nolint:unused +func (sh *statsHandlerImpl) getMethodWireBytesIn() map[string]uint64 { + wireIn := sh.wireBytesIn.RLock() + defer sh.wireBytesIn.RUnlock(&wireIn) + res := make(map[string]uint64, len(*wireIn)) + for k, v := range *wireIn { + res[k] = v + } + return res +} + +// Metrics holds the metric counters for the reporter package. +type Metrics struct { + CountsForTracesOverwriteCount uint32 + ExeMetadataOverwriteCount uint32 + FrameMetadataOverwriteCount uint32 + FramesForTracesOverwriteCount uint32 + HostMetadataOverwriteCount uint32 + MetricsOverwriteCount uint32 + FallbackSymbolsOverwriteCount uint32 + RPCBytesOutCount int64 + RPCBytesInCount int64 + 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 new file mode 100644 index 00000000..c75baa1b --- /dev/null +++ b/reporter/otlp_reporter.go @@ -0,0 +1,759 @@ +/* + * 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" + "fmt" + "time" + + "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" + + common "go.opentelemetry.io/proto/otlp/common/v1" + resource "go.opentelemetry.io/proto/otlp/resource/v1" + + lru "github.com/elastic/go-freelru" + "github.com/zeebo/xxh3" +) + +// 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. +type sourceInfo struct { + lineNumber libpf.SourceLineno + functionOffset uint32 + functionName string + filePath string +} + +// funcInfo is a helper to construct profile.Function messages. +type funcInfo struct { + name string + fileName string +} + +// OTLPReporter receives and transforms information to be OTLP/profiles compliant. +type OTLPReporter struct { + // client for the connection to the receiver. + client otlpcollector.ProfilesServiceClient + + // stopSignal is the stop signal for shutting down all background tasks. + stopSignal chan libpf.Void + + // rpcStats stores gRPC related statistics. + rpcStats *statsHandlerImpl + + // To fill in the OTLP/profiles signal with the relevant information, + // 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] + + // executables stores metadata for executables. + executables *lru.SyncedLRU[libpf.FileID, execInfo] + + // frames maps frame information to its source location. + frames *lru.SyncedLRU[libpf.FileID, map[libpf.AddressOrLineno]sourceInfo] +} + +// 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. +// 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, + }) + } +} + +// 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, + }) + } + + if v, ok := r.samples.Peek(traceHash); ok { + v.count += uint32(count) + v.timestamps = append(v.timestamps, uint64(timestamp)) + + r.samples.Add(traceHash, v) + } else { + r.samples.Add(traceHash, sample{ + count: uint32(count), + timestamps: []uint64{uint64(timestamp)}, + }) + } +} + +// ReportFallbackSymbol enqueues a fallback symbol for reporting, for a given frame. +func (r *OTLPReporter) ReportFallbackSymbol(frameID libpf.FrameID, symbol string) { + if _, exists := r.fallbackSymbols.Peek(frameID); exists { + return + } + r.fallbackSymbols.Add(frameID, symbol) +} + +// ExecutableMetadata accepts a fileID with the corresponding filename +// and caches this information. +func (r *OTLPReporter) ExecutableMetadata(_ context.Context, + fileID libpf.FileID, fileName, buildID string) { + r.executables.Add(fileID, execInfo{ + fileName: fileName, + buildID: buildID, + }) +} + +// 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 { + 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 { + filePath = s.filePath + } + } + v[addressOrLine] = sourceInfo{ + lineNumber: lineNumber, + functionOffset: functionOffset, + functionName: functionName, + filePath: filePath, + } + return + } + + v := make(map[libpf.AddressOrLineno]sourceInfo) + v[addressOrLine] = sourceInfo{ + lineNumber: lineNumber, + functionOffset: functionOffset, + functionName: functionName, + filePath: filePath, + } + r.frames.Add(fileID, v) +} + +// ReportHostMetadata enqueues host metadata. +func (r *OTLPReporter) ReportHostMetadata(metadataMap map[string]string) { + r.addHostmetadata(metadataMap) +} + +// ReportHostMetadataBlocking enqueues host metadata. +func (r *OTLPReporter) ReportHostMetadataBlocking(_ context.Context, + metadataMap map[string]string, _ int, _ time.Duration) error { + r.addHostmetadata(metadataMap) + return nil +} + +// addHostmetadata adds to and overwrites host metadata. +func (r *OTLPReporter) addHostmetadata(metadataMap map[string]string) { + for k, v := range metadataMap { + r.hostmetadata.Add(k, v) + } +} + +// ReportMetrics is a NOP for OTLPReporter. +func (r *OTLPReporter) ReportMetrics(_ uint32, _ []uint32, _ []int64) {} + +// Stop triggers a graceful shutdown of OTLPReporter. +func (r *OTLPReporter) Stop() { + close(r.stopSignal) +} + +// 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(), + } +} + +// StartOTLP sets up and manages the reporting connection to a OTLP backend. +func StartOTLP(mainCtx context.Context, c *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 + } + + executables, err := lru.NewSynced[libpf.FileID, execInfo](cacheSize, libpf.FileID.Hash32) + if err != nil { + return nil, err + } + + frames, err := lru.NewSynced[libpf.FileID, + 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 + // hostmetadata/hostmetadata.json. + hostmetadata, err := lru.NewSynced[string, string](115, hashString) + if err != nil { + return nil, err + } + + r := &OTLPReporter{ + stopSignal: make(chan libpf.Void), + client: nil, + rpcStats: newStatsHandler(), + traces: traces, + samples: samples, + fallbackSymbols: fallbackSymbols, + executables: executables, + frames: frames, + hostmetadata: hostmetadata, + } + + // Create a child context for reporting features + ctx, cancelReporting := context.WithCancel(mainCtx) + + // 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) + if err != nil { + cancelReporting() + close(r.stopSignal) + return nil, err + } + r.client = otlpcollector.NewProfilesServiceClient(otlpGrpcConn) + + go func() { + tick := time.NewTicker(c.Times.ReportInterval()) + defer tick.Stop() + for { + select { + case <-ctx.Done(): + return + case <-r.stopSignal: + return + case <-tick.C: + if err := r.reportOTLPProfile(ctx); err != nil { + log.Errorf("Request failed: %v", err) + } + tick.Reset(libpf.AddJitter(c.Times.ReportInterval(), 0.2)) + } + } + }() + + // When Stop() is called and a signal to 'stop' is received, then: + // - cancel the reporting functions currently running (using context) + // - close the gRPC connection with collection-agent + go func() { + <-r.stopSignal + cancelReporting() + if err := otlpGrpcConn.Close(); err != nil { + log.Fatalf("Stopping connection of OTLP client client failed: %v", err) + } + }() + + return r, nil +} + +// reportOTLPProfile creates and sends out an OTLP profile. +func (r *OTLPReporter) reportOTLPProfile(ctx context.Context) error { + profile, startTS, endTS := r.getProfile() + + if len(profile.Sample) == 0 { + log.Debugf("Skip sending of OTLP profile with no samples") + return nil + } + + 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()), + // Attributes - Optional element we do not use. + // DroppedAttributesCount - Optional element we do not use. + // OriginalPayloadFormat - Optional element we do not use. + // OriginalPayload - Optional element we do not use. + Profile: profile, + }} + + scopeProfiles := []*profiles.ScopeProfiles{{ + Profiles: pc, + Scope: &common.InstrumentationScope{ + Name: "Elastic-Universal-Profiling", + Version: fmt.Sprintf("%s@%s", vc.Version(), vc.Revision()), + }, + // 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. + }} + + req := otlpcollector.ExportProfilesServiceRequest{ + ResourceProfiles: resourceProfiles, + } + + _, err := r.client.Export(ctx, &req) + return err +} + +// 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, + 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 + } + samplesCpy[k] = v + r.samples.Remove(k) + } + + var samplesWoTraceinfo []libpf.TraceHash + + for trace := range samplesCpy { + if _, exists := r.traces.Peek(trace); !exists { + samplesWoTraceinfo = append(samplesWoTraceinfo, trace) + } + } + + 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) + } + } + + // stringMap is a temporary helper that will build the StringTable. + // By specification, the first element should be empty. + stringMap := make(map[string]uint32) + stringMap[""] = 0 + + // funcMap is a temporary helper that will build the Function array + // in profile and make sure information is deduplicated. + funcMap := make(map[funcInfo]uint64) + funcMap[funcInfo{name: "", fileName: ""}] = 0 + + numSamples := len(samplesCpy) + profile = &pprofextended.Profile{ + // SampleType - Next step: Figure out the correct SampleType. + Sample: make([]*pprofextended.Sample, 0, numSamples), + // LocationIndices - Optional element we do not use. + // AttributeTable - Optional element we do not use. + // AttributeUnits - Optional element we do not use. + // LinkTable - Optional element we do not use. + // DropFrames - Optional element we do not use. + // KeepFrames - Optional element we do not use. + // TimeNanos - Optional element we do not use. + // DurationNanos - Optional element we do not use. + // PeriodType - Optional element we do not use. + // Period - Optional element we do not use. + // Comment - Optional element we do not use. + // DefaultSampleType - Optional element we do not use. + } + + locationIndex := uint64(0) + + // Temporary lookup to reference existing Mappings. + fileIDtoMapping := make(map[libpf.FileID]uint64) + frameIDtoFunction := make(map[libpf.FrameID]uint64) + + for traceHash, sampleInfo := range samplesCpy { + sample := &pprofextended.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 + } + } + + // Walk every frame of the trace. + for i := range trace.frameTypes { + loc := &pprofextended.Location{ + // Id - Optional element we do not use. + TypeIndex: getStringMapIndex(stringMap, + trace.frameTypes[i].String()), + Address: uint64(trace.linenos[i]), + // IsFolded - Optional element we do not use. + // Attributes - Optional element we do not use. + } + + switch frameKind := trace.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 { + locationMappingIndex = tmpMappingIndex + } else { + idx := uint64(len(fileIDtoMapping)) + fileIDtoMapping[trace.files[i]] = idx + locationMappingIndex = idx + + execInfo, exists := r.executables.Get(trace.files[i]) + + // Next step: Select a proper default value, + // if the name of the executable is not known yet. + var fileName = "UNKNOWN" + if exists { + fileName = execInfo.fileName + } + + profile.Mapping = append(profile.Mapping, &pprofextended.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]), + Filename: int64(getStringMapIndex(stringMap, fileName)), + BuildId: int64(getStringMapIndex(stringMap, + trace.files[i].StringNoQuotes())), + BuildIdKind: *pprofextended.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. + // HasLineNumbers - Optional element we do not use. + // HasInlinedFrames - Optional element we do not use. + }) + } + 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{} + + if tmpFunctionIndex, exists := frameIDtoFunction[frameID]; exists { + line.FunctionIndex = tmpFunctionIndex + } else { + symbol, exists := r.fallbackSymbols.Get(frameID) + if !exists { + // TODO: choose a proper default value if the kernel symbol was not + // reported yet. + symbol = "UNKNOWN" + } + line.FunctionIndex = createFunctionEntry(funcMap, + symbol, "vmlinux") + } + loc.Line = append(loc.Line, line) + + // To be compliant with the protocol generate a dummy mapping entry. + loc.MappingIndex = getDummyMappingIndex(fileIDtoMapping, stringMap, + profile, trace.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 + // program. + default: + // Store interpreted frame information as Line message: + line := &pprofextended.Line{} + + fileIDInfo, exists := r.frames.Get(trace.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]] + 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 + // the file ID is available at all, we use a different name for reported + // function. + line.FunctionIndex = createFunctionEntry(funcMap, + "UNRESOLVED", frameKind.String()) + } else { + line.Line = int64(si.lineNumber) + + line.FunctionIndex = createFunctionEntry(funcMap, + si.functionName, si.filePath) + } + } + loc.Line = append(loc.Line, line) + + // To be compliant with the protocol generate a dummy mapping entry. + loc.MappingIndex = getDummyMappingIndex(fileIDtoMapping, stringMap, + profile, trace.files[i]) + } + profile.Location = append(profile.Location, loc) + } + + sample.Label = getTraceLabels(stringMap, trace) + sample.LocationsLength = uint64(len(trace.frameTypes)) + locationIndex += sample.LocationsLength + + 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)) + for v, idx := range funcMap { + funcTable[idx] = &pprofextended.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 + // hash value of the key. To get the correct order for profile.StringTable, + // 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 = make([]int64, len(profile.Location)) + for i := int64(0); i < int64(len(profile.Location)); i++ { + profile.LocationIndices[i] = i + } + + return profile, startTS, endTS +} + +// getStringMapIndex inserts or looks up the index for value in stringMap. +func getStringMapIndex(stringMap map[string]uint32, value string) uint32 { + if idx, exists := stringMap[value]; exists { + return idx + } + + idx := uint32(len(stringMap)) + stringMap[value] = idx + + return idx +} + +// createFunctionEntry adds a new function and returns its reference index. +func createFunctionEntry(funcMap map[funcInfo]uint64, + name string, fileName string) uint64 { + key := funcInfo{ + name: name, + fileName: fileName, + } + if idx, exists := funcMap[key]; exists { + return idx + } + + idx := uint64(len(funcMap)) + funcMap[key] = idx + + 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) + + 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), + }) + } + + if i.apmServiceName != "" { + apmServiceNameIdx := getStringMapIndex(stringMap, "apmServiceName") + apmServiceNameValueIdx := getStringMapIndex(stringMap, i.apmServiceName) + + labels = append(labels, &pprofextended.Label{ + Key: int64(apmServiceNameIdx), + Str: int64(apmServiceNameValueIdx), + }) + } + + return labels +} + +// getDummyMappingIndex inserts or looks up a dummy entry for interpreted FileIDs. +func getDummyMappingIndex(fileIDtoMapping map[libpf.FileID]uint64, + stringMap map[string]uint32, profile *pprofextended.Profile, + fileID libpf.FileID) uint64 { + var locationMappingIndex uint64 + if tmpMappingIndex, exists := fileIDtoMapping[fileID]; exists { + locationMappingIndex = tmpMappingIndex + } else { + idx := uint64(len(fileIDtoMapping)) + fileIDtoMapping[fileID] = idx + locationMappingIndex = idx + + fileName := "DUMMY" + + profile.Mapping = append(profile.Mapping, &pprofextended.Mapping{ + Filename: int64(getStringMapIndex(stringMap, fileName)), + BuildId: int64(getStringMapIndex(stringMap, + fileID.StringNoQuotes())), + BuildIdKind: *pprofextended.BuildIdKind_BUILD_ID_BINARY_HASH.Enum(), + }) + } + return locationMappingIndex +} diff --git a/reporter/reporter.go b/reporter/reporter.go new file mode 100644 index 00000000..b297d7dd --- /dev/null +++ b/reporter/reporter.go @@ -0,0 +1,239 @@ +/* + * 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/testdata/.gitignore b/reporter/testdata/.gitignore new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/reporter/testdata/.gitignore @@ -0,0 +1 @@ +test diff --git a/reporter/testdata/Makefile b/reporter/testdata/Makefile new file mode 100644 index 00000000..18c7baa3 --- /dev/null +++ b/reporter/testdata/Makefile @@ -0,0 +1,12 @@ +.PHONY: all + +CC=gcc +CFLAGS=-Wl,--build-id + +all: test + +clean: + rm -f test + +test: test.c + $(CC) $(CFLAGS) $< -g -o $@ diff --git a/reporter/testdata/test.c b/reporter/testdata/test.c new file mode 100644 index 00000000..43ba4dc4 --- /dev/null +++ b/reporter/testdata/test.c @@ -0,0 +1,6 @@ +#include + +int main(int argc, char *argv[]) { + // This process must not return (tests depend on it) + return pause(); +} diff --git a/support/PYTHON_LINENO_RECOVERY_README.md b/support/PYTHON_LINENO_RECOVERY_README.md new file mode 100644 index 00000000..2d06969b --- /dev/null +++ b/support/PYTHON_LINENO_RECOVERY_README.md @@ -0,0 +1,101 @@ +# Building a Backtrace for Python Code + +Python code objects do not directly store the line number they are associated with. There +is a field `f_lineno` inside a `PyFrameObject`, but this only stores the line number if +tracing is activated. Instead, we can retrieve the bytecode offset of the current +instruction from the `f_lasti` field of the frame object. The code object then contains a +field called `co_lnotab`, which allows one to map from bytecode offsets to line numbers. +The process of mapping bytecode offsets to line numbers is described in +[this][lnotab-notes] file. Note that this representation may change between Python +versions. The code in this repository is written as of Python 3.7.1. + +In the interpreter, the `PyCode_Addr2Line` function provides an implementation of this +mapping. A python implementation can be found in Python's gdb [helpers][libpython]. +Further information on code objects can be found [here][code objects]. + +## Difficulties in Determining the Correct Line Number + +A potential edge case on mapping offsets to line numbers is mentioned in the +[notes][lnotab-notes] on tracing. It seems that if a function has an implicit return then +the offset for the opcodes associated with that return may map to a line number for Python +code that is not actually being executed. For example, in the notes this function is +presented (see the notes for the bytecode): + +```python +1: def f(a): +2: while a: +3: print(1) +4: break +5: else: +6: print(2) +``` + +The opcode for returning from the function is generated and associated with line 6. If we +reach line 4 we then jump to the return opcode. At this point, if we attempt to map the +opcode to a line number we will incorrectly deduce that we are at line 6. Similarly, a +single opcode will be generated to break from the loop. It will be jumped to if `a` is +false on line 2, or by reaching line 4. If, say, `a` is false we jump to the `BREAK_LOOP` +opcode, but at this point if we try to determine the line number we will incorrectly +deduce that we are at line 4, not 2. + +I am not sure how to deal with such situations yet. It is possible that we can't, and +just have to accept that in these limited cases we may assign a frame to the wrong line +number. + +## Mapping Bytecode Offsets to Line Numbers in Userspace + +There appears to be no upper limit on the number of lines of code that a code object may +represent. Calculating line number information therefore requires a loop who's number of +iterations has a very large upper bound. In eBPF we can use tail-calls to get around the +size limit of 4906 instructions per program, but there is still an upper limit of 32 such +calls. + +Instead of trying to map the bytecode offset to a line number in the eBPF program it is +likely better to do it in userspace. The .pyc files associated with each Python source +file contain `PyCodeObject` instances, that in turn contain the `co_lnotab` tables that +we require to perform the mapping from bytecode offsets to line numbers. The `dis` module +provides functions for processing compiled Python code, so if we log the bytecode offsets +in kernel space we may be able to use functions from `dis` to recover the line number +information. + +Given a .pyc file we can construct a code object using `dis`. Code objects can be nested, +so the code object for a module may contain code objects for classes, which in turn may +contain code objects for functions and so on. Thus we need the eBPF side of things to log +some piece of information that uniquely identifies the code object associated with the +bytecode offset it is logging. The `co_firstlineno` field of a `PyCodeObject` seems like a +good candidate for this. It provides the first source line number with which the code +object is associated. Unfortunately, it is not unique. For example, in the following code +there may be two code objects created, both that have a `co_firstlineno` value of 1. + +```python +def foo(): + for x in range(100): + pass +``` + +The first code object will look as follows. It creates the function and associates it +with the correct name. + +``` +1 0 LOAD_CONST 0 () + 2 LOAD_CONST 1 ('foo') + 4 MAKE_FUNCTION 0 + 6 STORE_NAME 0 (foo) +``` + +Another code object will then be created to provide the actual code of the function that +is referenced at the first opcode. + +To get around the above issue, we identify code objects using a hash of several fields. +For the exact fields, see the Python tracer. At the moment the are `co_firstlineno`, +`co_argcount`, `co_kwonlyargcount` and `co_flags`. + +Once we have found the correct code object, we can then generate a list of `(offset, +lineno)` pairs using the `dis.findlinestarts(code)` function. With this we can then +convert offsets to line numbers. In our real version we can build a multi-level map ahead +of time, to enable converting a `(filename, co_firstlineno, bytecode offset)` triple to a +line number. + +[lnotab-notes]: https://github.com/python/cpython/blob/37788bc23f6f1ed0362b9b3b248daf296c024849/Objects/lnotab_notes.txt +[libpython]: https://github.com/python/cpython/blob/37788bc23f6f1ed0362b9b3b248daf296c024849/Tools/gdb/libpython.py#L642 +[code objects]: https://leanpub.com/insidethepythonvirtualmachine/read#leanpub-auto-code-objects diff --git a/support/README.md b/support/README.md new file mode 100644 index 00000000..181646b4 --- /dev/null +++ b/support/README.md @@ -0,0 +1,63 @@ +This directory is intended for non-Go support functionality. For example, the +eBPF code and supporting Python scripts for bytecode offsets to line number +translation. + +## Testing eBPF code on different kernel version +Via the following commands, you can run the eBPF loading tests on kernel version +4.9.198, 4.19.81 or 5.4.5 respectively. +``` +$ ./run-tests.sh 4.9.198 +$ ./run-tests.sh 4.19.81 +$ ./run-tests.sh 5.4.5 +``` +The script loads the provided eBPF code into the kernel in a virtual environment so that it does not affect your local environment. + +## Requirements +The tests are built on top of the following dependencies. Make sure you have them installed beforehand. + + * qemu-system-x86 + * statically linked busybox + + ## Building a Custom Kernel Image + Kernel images can be build with the script provided in `ci-kernels`. This directory contains also the basic configuration settings needed to enable eBPF features for the kernel image. + + ## Test a Custom Kernel Image + By default `run-tests.sh` takes only the kernel version as argument. The script looks for the kernel image with the specified version in `ci-kernels`. As an alternative one can provide a directory to look for this kernel image via `KERN_DIR`. + ``` + $ KERN_DIR=my-other-kernels/ ./run-tests.sh 5.4.31 + ``` + + ## Manually Debugging a Custom Kernel Image +1. Compile eBPF and Go code +``` +$ make ebpf +$ cd support +$ go test -c -tags integration ./... +``` +2. Get [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git/) to run the environment +``` +$ tmp_virtme="$(mktemp -d --suffix=-virtme)" +$ git clone -q https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git "${tmp_virtme}" +``` +3. Start the virtual environment for debugging with gdb: +``` +$ ${tmp_virtme}/virtme-run --kimg ci-kernels/linux-5.4.31.bz \ + --memory 4096M \ + --pwd \ + --script-sh "mount -t bpf bpf /sys/fs/bpf ; ./support.test -test.v" \ + --qemu-opts -append nokaslr -s +``` +4. Start gdb in a second shell: +``` +$ cd support +$ gdb +# Attach gdb to the running qemu process in the same directory: +(gdb) target remote localhost:1234 +# Load source code: +(gdb) directory ./ci-kernels/_build/linux-5.4.31 +# Load symbols for debugging: +(gdb) sym ./ci-kernels/_build/linux-5.4.31/vmlinux +# Set breakpoint at entry of eBPF verifier: +(gdb) break do_check +Breakpoint 1 at 0xffffffff81184460: file kernel/bpf/verifier.c, line 4105. +``` diff --git a/support/ebpf/COPYING b/support/ebpf/COPYING new file mode 100644 index 00000000..3dfbbdbe --- /dev/null +++ b/support/ebpf/COPYING @@ -0,0 +1,15 @@ +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/LICENSE b/support/ebpf/LICENSE new file mode 100644 index 00000000..10828e06 --- /dev/null +++ b/support/ebpf/LICENSE @@ -0,0 +1,341 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + 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 + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/support/ebpf/Makefile b/support/ebpf/Makefile new file mode 100644 index 00000000..f3ab9dd9 --- /dev/null +++ b/support/ebpf/Makefile @@ -0,0 +1,91 @@ +SHELL:=/usr/bin/env bash +KERNEL_HEADERS ?= /lib/modules/$(shell uname -r) + +CLANG=clang +LINK=llvm-link +LLC=llc + +DEBUG_FLAGS = -DOPTI_DEBUG -g + +# Detect native architecture. +NATIVE_ARCH:=$(shell uname -m) + +ifeq ($(NATIVE_ARCH),x86_64) +NATIVE_ARCH:=x86 +else ifeq ($(NATIVE_ARCH),aarch64) +NATIVE_ARCH:=arm64 +else +$(error Unsupported architecture: $(NATIVE_ARCH)) +endif + +# This can be passed-in, valid values are: x86, arm64. +target_arch ?= $(NATIVE_ARCH) + +# Set default values. +TARGET_ARCH = $(target_arch) +TRACER_NAME = tracer.ebpf.$(TARGET_ARCH) + +ifeq ($(TARGET_ARCH),arm64) +TARGET_FLAGS = -target aarch64-linux-gnu +else +TARGET_FLAGS = -target x86_64-linux-gnu +endif + +FLAGS=-D__KERNEL__ \ + -D__BPF_TRACING__ \ + $(TARGET_FLAGS) \ + -O2 -emit-llvm -c $< \ + -Wall -Wextra -Werror \ + -Wno-address-of-packed-member \ + -Wno-unused-label \ + -Wno-unused-parameter \ + -Wno-sign-compare \ + -fno-stack-protector \ + -fno-jump-tables \ + -isystem $(KERNEL_HEADERS)/source/arch/$(TARGET_ARCH)/include \ + -isystem $(KERNEL_HEADERS)/source/arch/$(TARGET_ARCH)/include/generated \ + -isystem $(KERNEL_HEADERS)/build/include \ + -isystem $(KERNEL_HEADERS)/build/include/uapi \ + -isystem $(KERNEL_HEADERS)/build/arch/$(TARGET_ARCH)/include \ + -isystem $(KERNEL_HEADERS)/build/arch/$(TARGET_ARCH)/include/generated \ + -isystem $(KERNEL_HEADERS)/source/include + +SRCS := $(wildcard *.ebpf.c) +OBJS := $(SRCS:.c=.o) + +.DEFAULT_GOAL := all + +all: $(TRACER_NAME) + +debug: TARGET_FLAGS+=$(DEBUG_FLAGS) +debug: all + +x86: + $(MAKE) target_arch=x86 all + +arm64: + $(MAKE) target_arch=arm64 all + +debug-x86: + $(MAKE) target_arch=x86 debug + +debug-arm64: + $(MAKE) target_arch=arm64 debug + +%.ebpf.c: errors.h ; + +%.ebpf.o: %.ebpf.c + $(CLANG) $(FLAGS) -o $@ + +$(TRACER_NAME): $(OBJS) + $(LINK) $^ -o - | $(LLC) -march=bpf -mcpu=v2 -filetype=obj -o $@ + @./print_instruction_count.sh $@ + +baseline: $(TRACER_NAME) + cp $< $(TRACER_NAME).$@ + +bloatcheck: $(TRACER_NAME) + python3 bloat-o-meter $(TRACER_NAME).baseline $(TRACER_NAME) + +clean: + rm -f *.o $(TRACER_NAME) $(TRACER_NAME).* diff --git a/support/ebpf/bloat-o-meter b/support/ebpf/bloat-o-meter new file mode 100755 index 00000000..1cdc820a --- /dev/null +++ b/support/ebpf/bloat-o-meter @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# +# Copyright 2004 Matt Mackall +# +# Inspired by perl Bloat-O-Meter (c) 1997 by Andi Kleen +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +import sys, os + +def usage(): + sys.stderr.write("usage: %s [-t] file1 file2 [-- ]\n" + % sys.argv[0]) + sys.stderr.write("\t-t\tShow time spent on parsing/processing\n") + sys.stderr.write("\t--\tPass additional parameters to readelf\n") + sys.exit(1) + +f1, f2 = (None, None) +flag_timing, dashes = (False, False) + +for f in sys.argv[1:]: + if f.startswith("-"): + if f == "--": # sym_args + dashes = True + break + if f == "-t": # timings + flag_timing = True + else: + if not os.path.exists(f): + sys.stderr.write("Error: file '%s' does not exist\n" % f) + usage() + if f1 is None: + f1 = f + elif f2 is None: + f2 = f + else: + usage() +if flag_timing: + import time +if f1 is None or f2 is None: + usage() + +sym_args = " ".join(sys.argv[3 + flag_timing + dashes:]) +def getsizes(file): + sym, alias = {}, {} + for l in os.popen("readelf -W -s %s %s" % (sym_args, file)).readlines(): + l = l.strip() + if not (len(l) and l[0].isdigit() and len(l.split()) == 8): + continue + num, value, size, typ, bind, vis, ndx, name = l.split() + if ndx == "UND": continue # skip undefined + if typ in ["SECTION", "FILES"]: continue # skip sections and files + if "." in name: name = "static." + name.split(".")[0] + value = int(value, 16) + size = int(size, 16) if size.startswith('0x') else int(size) + if vis == "DEFAULT" or bind == "GLOBAL": + sym[name] = {"addr" : value, "insn" : size // 8 } + return sym + +if flag_timing: + start_t1 = int(time.time() * 1e9) +old = getsizes(f1) +if flag_timing: + end_t1 = int(time.time() * 1e9) + start_t2 = int(time.time() * 1e9) +new = getsizes(f2) +if flag_timing: + end_t2 = int(time.time() * 1e9) + start_t3 = int(time.time() * 1e9) +grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0 +delta, common = [], {} + +for name in iter(old.keys()): + if name in new: + common[name] = 1 + +for name in old: + if name not in common: + remove += 1 + insn = old[name]["insn"] + down += insn + delta.append((-insn, name)) + +for name in new: + if name not in common: + add += 1 + insn = new[name]["insn"] + up += insn + delta.append((insn, name)) + +for name in common: + d = new[name].get("insn", 0) - old[name].get("insn", 0) + if d>0: grow, up = grow+1, up+d + elif d<0: shrink, down = shrink+1, down-d + else: + continue + delta.append((d, name)) + +delta.sort() +delta.reverse() +if flag_timing: + end_t3 = int(time.time() * 1e9) + +print("%-48s %7s %7s %+7s" % ("function", "old", "new", "delta")) +for d, n in delta: + if d: + old_insn = old.get(n, {}).get("insn", "-") + new_insn = new.get(n, {}).get("insn", "-") + print("%-48s %7s %7s %+7d" % (n, old_insn, new_insn, d)) +print("-"*78) +total="(add/remove: %s/%s grow/shrink: %s/%s up/down: %s/%s)%%sTotal: %s insns"\ + % (add, remove, grow, shrink, up, -down, up-down) +print(total % (" "*(80-len(total)))) +if flag_timing: + print("\n%d/%d; %d Parse origin/new; processing nsecs" % + (end_t1-start_t1, end_t2-start_t2, end_t3-start_t3)) + print("total nsecs: %d" % (end_t3-start_t1)) diff --git a/support/ebpf/bpf_map.h b/support/ebpf/bpf_map.h new file mode 100644 index 00000000..48d408f1 --- /dev/null +++ b/support/ebpf/bpf_map.h @@ -0,0 +1,17 @@ +#ifndef OPTI_BPF_MAP_H +#define OPTI_BPF_MAP_H + +// bpf_map_def is a custom struct we use to define eBPF maps. It is not used by +// the kernel, but by the ebpf loader (kernel tools, gobpf, cilium-ebpf, etc.). +// This version matches with cilium-ebpf. + +typedef struct bpf_map_def { + unsigned int type; + unsigned int key_size; + unsigned int value_size; + unsigned int max_entries; + unsigned int map_flags; + unsigned int pinning; +} bpf_map_def; + +#endif // OPTI_BPF_MAP_H diff --git a/support/ebpf/bpfdefs.h b/support/ebpf/bpfdefs.h new file mode 100644 index 00000000..a3129944 --- /dev/null +++ b/support/ebpf/bpfdefs.h @@ -0,0 +1,231 @@ +#ifndef OPTI_BPFDEFS_H +#define OPTI_BPFDEFS_H + +#include "bpf_map.h" +#include "inttypes.h" + +#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. + #include // BPF_* defines + #include // pid_t + #include // uintptr_t + #define SEC(NAME) + + #define printt(fmt, ...) bpf_log(fmt, ##__VA_ARGS__) + #define DEBUG_PRINT(fmt, ...) bpf_log(fmt, ##__VA_ARGS__) + #define OPTI_DEBUG + + // The following works with clang and gcc. + // Checked with + // clang -dM -E -x c /dev/null | grep ENDI + // gcc -dM -E -x c /dev/null | grep ENDI + #if defined __BYTE_ORDER__ && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + #include + #define __constant_cpu_to_be32(x) __bswap_32(x) + #define __constant_cpu_to_be64(x) __bswap_64(x) + #elif defined __BYTE_ORDER__ && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + #define __constant_cpu_to_be32(x) (x) + #define __constant_cpu_to_be64(x) (x) + #else + #error "Unknown endianness" + #endif + + // 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. + + // defined in arch/x86/include/asm/ptrace.h + + #if defined(__x86_64) + struct pt_regs { + unsigned long r15; + unsigned long r14; + unsigned long r13; + unsigned long r12; + unsigned long bp; + unsigned long bx; + unsigned long r11; + unsigned long r10; + unsigned long r9; + unsigned long r8; + unsigned long ax; + unsigned long cx; + unsigned long dx; + unsigned long si; + unsigned long di; + unsigned long orig_ax; + unsigned long ip; + unsigned long cs; + unsigned long flags; + unsigned long sp; + unsigned long ss; + }; + + #define reg_pc ip + + #elif defined(__aarch64__) + + struct pt_regs { + u64 regs[31]; + u64 sp; + u64 pc; + u64 pstate; + u64 orig_x0; + s32 syscallno; + u32 unused2; + u64 sdei_ttbr1; + u64 pmr_save; + u64 stackframe[2]; + u64 lockdep_hardirqs; + u64 exit_rcu; + }; + + #define reg_pc pc + + #else + #error "Unsupported architecture" + #endif + + struct bpf_perf_event_data { + struct pt_regs regs; + }; + + // 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 u64 bpf_get_current_pid_tgid(void) { + return __cgo_ctx->id; + } + + static inline void *bpf_map_lookup_elem(bpf_map_def *map, const void *key) { + void *__bpf_map_lookup_elem(u64, bpf_map_def *, const void *); + return __bpf_map_lookup_elem(__cgo_ctx->id, map, key); + } + + static inline int bpf_map_update_elem(bpf_map_def *map, const void *key, const void *val, + u64 flags) { + return -1; + } + + static inline int bpf_map_delete_elem(bpf_map_def *map, const void *key) { + return -1; + } + + static inline int bpf_perf_event_output(void *ctx, bpf_map_def *mapdef, unsigned long long flags, + void *data, int size) { + return 0; + } + + static inline int bpf_get_stackid(void *ctx, bpf_map_def *map, u64 flags) { + return -1; + } + +#else // TESTING_COREDUMP + +// Native eBPF build + +#include // atomic64_t +// Linux 5.4 introduces asm_inline which clang cannot deal with. Disable it. +#undef CONFIG_CC_HAS_ASM_INLINE +#include + +#include + +// definitions of bpf helper functions we need, as found in +// https://elixir.bootlin.com/linux/v4.11/source/samples/bpf/bpf_helpers.h + +static void *(*bpf_map_lookup_elem)(void *map, void *key) = + (void *)BPF_FUNC_map_lookup_elem; +static int (*bpf_map_update_elem)(void *map, void *key, void *value, u64 flags) = + (void *)BPF_FUNC_map_update_elem; +static int (*bpf_map_delete_elem)(void *map, void *key) = + (void *)BPF_FUNC_map_delete_elem; +static int (*bpf_probe_read)(void *dst, int size, const void *unsafe_ptr) = + (void *)BPF_FUNC_probe_read; +static unsigned long long (*bpf_ktime_get_ns)(void) = + (void *)BPF_FUNC_ktime_get_ns; +static unsigned long long (*bpf_get_current_pid_tgid)(void) = + (void *)BPF_FUNC_get_current_pid_tgid; +static int (*bpf_get_current_comm)(void *buf, int buf_size) = + (void *) BPF_FUNC_get_current_comm; +static void (*bpf_tail_call)(void *ctx, void *map, int index) = + (void *)BPF_FUNC_tail_call; +static unsigned long long (*bpf_get_current_task)(void) = + (void *)BPF_FUNC_get_current_task; +static int (*bpf_perf_event_output)(void *ctx, void *map, unsigned long long flags, void *data, int size) = + (void *)BPF_FUNC_perf_event_output; +static int (*bpf_get_stackid)(void *ctx, void *map, u64 flags) = + (void *)BPF_FUNC_get_stackid; + +__attribute__ ((format (printf, 1, 3))) +static int (*bpf_trace_printk)(const char *fmt, int fmt_size, ...) = + (void *)BPF_FUNC_trace_printk; + +// 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. +#define printt(fmt, ...) \ + ({ \ + const char ____fmt[] = fmt "\n"; \ + bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \ + }) + +#ifdef OPTI_DEBUG + #define DEBUG_PRINT(fmt, ...) printt(fmt, ##__VA_ARGS__); + + // Sends `SIGTRAP` to the current task, killing it and capturing a coredump. + // + // Only use this in code paths that you expect to be hit by a very specific process that you + // intend to debug. Placing it into frequently taken code paths might otherwise take down + // important system processes like sshd or your window manager. For frequently taken cases, + // prefer using the `DEBUG_CAPTURE_COREDUMP_IF_TGID` macro. + // + // This macro requires linking against kernel headers >= 5.6. + #define DEBUG_CAPTURE_COREDUMP() \ + ({ \ + /* We don't define `bpf_send_signal_thread` globally because it requires a */ \ + /* rather recent kernel (>= 5.6) and otherwise breaks builds of older versions. */ \ + long (*bpf_send_signal_thread)(u32 sig) = (void *)BPF_FUNC_send_signal_thread; \ + bpf_send_signal_thread(SIGTRAP); \ + }) + + // Like `DEBUG_CAPTURE_COREDUMP`, but only coredumps if the current task is a member of the given + // thread group ID ("process"). + #define DEBUG_CAPTURE_COREDUMP_IF_TGID(tgid) \ + ({ \ + if (bpf_get_current_pid_tgid() >> 32 == (tgid)) { \ + DEBUG_PRINT("coredumping process %d", (tgid)); \ + DEBUG_CAPTURE_COREDUMP(); \ + } \ + }) +#else + #define DEBUG_PRINT(fmt, ...) + #define DEBUG_CAPTURE_COREDUMP() + #define DEBUG_CAPTURE_COREDUMP_IF_TGID(tgid) +#endif + +// Definition of SEC as used by the Linux kernel in tools/lib/bpf/bpf_helpers.h for clang compilations. +#define SEC(name) \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wignored-attributes\"") \ + __attribute__((section(name), used)) \ + _Pragma("GCC diagnostic pop") + +#endif // !TESTING_COREDUMP + +#define ATOMIC_ADD(ptr, n) __sync_fetch_and_add(ptr, n) + +#endif // OPTI_BPFDEFS_H diff --git a/support/ebpf/errors.h b/support/ebpf/errors.h new file mode 100644 index 00000000..d4ad52d5 --- /dev/null +++ b/support/ebpf/errors.h @@ -0,0 +1,152 @@ +/* WARNING: this file is auto-generated, DO NOT CHANGE MANUALLY */ + +#ifndef OPTI_ERRORS_H +#define OPTI_ERRORS_H + +typedef enum ErrorCode { + // Sentinel value for success: not actually an error + ERR_OK = 0, + + // Entered code that was believed to be unreachable + ERR_UNREACHABLE = 1, + + // The stack trace has reached its maximum length and could not be unwound further + ERR_STACK_LENGTH_EXCEEDED = 2, + + // The trace stack was empty after unwinding completed + ERR_EMPTY_STACK = 3, + + // Failed to lookup entry in the per-CPU frame list + ERR_LOOKUP_PER_CPU_FRAME_LIST = 4, + + // Maximum number of tail calls was reached + ERR_MAX_TAIL_CALLS = 5, + + // Hotspot: Failure to get CodeBlob address (no heap or bad segmap) + ERR_HOTSPOT_NO_CODEBLOB = 1000, + + // Hotspot: Failure to unwind interpreter due to invalid FP + ERR_HOTSPOT_INTERPRETER_FP = 1001, + + // Hotspot: Failure to unwind because return address was not found with heuristic + ERR_HOTSPOT_INVALID_RA = 1002, + + // Hotspot: Failure to get codeblob data or matching it to current unwind state + ERR_HOTSPOT_INVALID_CODEBLOB = 1003, + + // Hotspot: Unwind instructions requested LR unwinding mid-trace (nonsensical) + ERR_HOTSPOT_LR_UNWINDING_MID_TRACE = 1004, + + // Python: Unable to read current PyCodeObject + ERR_PYTHON_BAD_CODE_OBJECT_ADDR = 2000, + + // Python: No entry for this process exists in the Python process info array + ERR_PYTHON_NO_PROC_INFO = 2001, + + // Python: Unable to read current PyFrameObject + ERR_PYTHON_BAD_FRAME_OBJECT_ADDR = 2002, + + // Python: Unable to read _PyCFrame.current_frame + ERR_PYTHON_BAD_CFRAME_CURRENT_FRAME_ADDR = 2003, + + // Python: Unable to read the thread state pointer from TLD + ERR_PYTHON_READ_THREAD_STATE_ADDR = 2004, + + // Python: The thread state pointer read from TSD is zero + ERR_PYTHON_ZERO_THREAD_STATE = 2005, + + // Python: Unable to read the frame pointer from the thread state object + ERR_PYTHON_BAD_THREAD_STATE_FRAME_ADDR = 2006, + + // Python: Unable to read autoTLSkey + ERR_PYTHON_BAD_AUTO_TLS_KEY_ADDR = 2007, + + // Python: Unable to determine the base address for thread-specific data + ERR_PYTHON_READ_TSD_BASE = 2008, + + // Ruby: No entry for this process exists in the Ruby process info array + ERR_RUBY_NO_PROC_INFO = 3000, + + // Ruby: Unable to read the stack pointer from the Ruby context + ERR_RUBY_READ_STACK_PTR = 3001, + + // Ruby: Unable to read the size of the VM stack from the Ruby context + ERR_RUBY_READ_STACK_SIZE = 3002, + + // Ruby: Unable to read the control frame pointer from the Ruby context + ERR_RUBY_READ_CFP = 3003, + + // Ruby: Unable to read the expression path from the Ruby frame + ERR_RUBY_READ_EP = 3004, + + // Ruby: Unable to read instruction sequence body + ERR_RUBY_READ_ISEQ_BODY = 3005, + + // Ruby: Unable to read the instruction sequence encoded size + ERR_RUBY_READ_ISEQ_ENCODED = 3006, + + // Ruby: Unable to read the instruction sequence size + ERR_RUBY_READ_ISEQ_SIZE = 3007, + + // Native: Unable to find the code section in the stack delta page info map + ERR_NATIVE_LOOKUP_TEXT_SECTION = 4000, + + // Native: Unable to look up the outer stack delta map (invalid map ID) + ERR_NATIVE_LOOKUP_STACK_DELTA_OUTER_MAP = 4001, + + // Native: Unable to look up the inner stack delta map (unknown text section ID) + ERR_NATIVE_LOOKUP_STACK_DELTA_INNER_MAP = 4002, + + // Native: Exceeded the maximum number of binary search steps during stack delta lookup + ERR_NATIVE_EXCEEDED_DELTA_LOOKUP_ITERATIONS = 4003, + + // Native: Unable to look up the stack delta from the inner map + ERR_NATIVE_LOOKUP_RANGE = 4004, + + // Native: The stack delta read from the delta map is marked as invalid + ERR_NATIVE_STACK_DELTA_INVALID = 4005, + + // Native: The stack delta read from the delta map is a stop record + ERR_NATIVE_STACK_DELTA_STOP = 4006, + + // Native: Unable to read the next instruction pointer from memory + ERR_NATIVE_PC_READ = 4007, + + // Native: Unwind instructions requested LR unwinding mid-trace (nonsensical) + ERR_NATIVE_LR_UNWINDING_MID_TRACE = 4008, + + // Native: Unable to read the kernel-mode registers + ERR_NATIVE_READ_KERNELMODE_REGS = 4009, + + // Native: Unable to read the IRQ stack link + ERR_NATIVE_CHASE_IRQ_STACK_LINK = 4010, + + // Native: Unexpectedly encountered a kernel mode pointer while attempting to unwind user-mode stack + ERR_NATIVE_UNEXPECTED_KERNEL_ADDRESS = 4011, + + // Native: Unable to locate the PID page mapping for the current instruction pointer + ERR_NATIVE_NO_PID_PAGE_MAPPING = 4012, + + // Native: Unexpectedly encountered a instruction pointer of zero + ERR_NATIVE_ZERO_PC = 4013, + + // Native: The instruction pointer is too small to be valid + ERR_NATIVE_SMALL_PC = 4014, + + // Native: Encountered an invalid unwind_info_array index + ERR_NATIVE_BAD_UNWIND_INFO_INDEX = 4015, + + // Native: Code is running in ARM 32-bit compat mode. + ERR_NATIVE_AARCH64_32BIT_COMPAT_MODE = 4016, + + // V8: Encountered a bad frame pointer during V8 unwinding + ERR_V8_BAD_FP = 5000, + + // V8: The JavaScript function object read from memory is invalid + 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 +} ErrorCode; + +#endif // OPTI_ERRORS_H diff --git a/support/ebpf/extmaps.h b/support/ebpf/extmaps.h new file mode 100644 index 00000000..28f2b8ac --- /dev/null +++ b/support/ebpf/extmaps.h @@ -0,0 +1,53 @@ +// References to the map definitions in the BPF C code. + +#ifndef OPTI_EXTMAPS_H +#define OPTI_EXTMAPS_H + +#include "bpf_map.h" + +// References to map definitions in *.ebpf.c. +extern bpf_map_def progs; +extern bpf_map_def per_cpu_records; +extern bpf_map_def pid_page_to_mapping_info; +extern bpf_map_def metrics; +extern bpf_map_def report_events; +extern bpf_map_def reported_pids; +extern bpf_map_def pid_events; +extern bpf_map_def inhibit_events; +extern bpf_map_def interpreter_offsets; +extern bpf_map_def system_config; + +#if defined(TESTING_COREDUMP) + +// References to maps in alphabetical order that +// are needed only for testing. + +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; +extern bpf_map_def exe_id_to_11_stack_deltas; +extern bpf_map_def exe_id_to_12_stack_deltas; +extern bpf_map_def exe_id_to_13_stack_deltas; +extern bpf_map_def exe_id_to_14_stack_deltas; +extern bpf_map_def exe_id_to_15_stack_deltas; +extern bpf_map_def exe_id_to_16_stack_deltas; +extern bpf_map_def exe_id_to_17_stack_deltas; +extern bpf_map_def exe_id_to_18_stack_deltas; +extern bpf_map_def exe_id_to_19_stack_deltas; +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 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; +extern bpf_map_def unwind_info_array; +extern bpf_map_def v8_procs; + +#endif // TESTING_COREDUMP + +#endif // OPTI_EXTMAPS_H diff --git a/support/ebpf/frametypes.h b/support/ebpf/frametypes.h new file mode 100644 index 00000000..0435d3bd --- /dev/null +++ b/support/ebpf/frametypes.h @@ -0,0 +1,45 @@ +// Provides the frame type markers so they can be included by both +// the Go and eBPF components. +// +// NOTE: As this is included by both kernel and user-land components, do not +// include any files that cannot be included in both contexts. + +#ifndef OPTI_FRAMETYPES_H +#define OPTI_FRAMETYPES_H + +// Defines the bit mask that, when ORed with it, turn any of the below +// frame types into an error frame. +#define FRAME_MARKER_ERROR_BIT 0x80 + +// Indicates that the interpreter/runtime this frame belongs to is unknown. +#define FRAME_MARKER_UNKNOWN 0x0 +// Indicates a Python frame +#define FRAME_MARKER_PYTHON 0x1 +// Indicates a PHP frame +#define FRAME_MARKER_PHP 0x2 +// Indicates a native frame +#define FRAME_MARKER_NATIVE 0x3 +// Indicates a kernel frame +#define FRAME_MARKER_KERNEL 0x4 +// Indicates a HotSpot frame +#define FRAME_MARKER_HOTSPOT 0x5 +// Indicates a Ruby frame +#define FRAME_MARKER_RUBY 0x6 +// Indicates a Perl frame +#define FRAME_MARKER_PERL 0x7 +// Indicates a V8 frame +#define FRAME_MARKER_V8 0x8 +// Indicates a PHP JIT frame +#define FRAME_MARKER_PHP_JIT 0x9 + +// Indicates a frame containing information about a critical unwinding error +// that caused further unwinding to be aborted. +#define FRAME_MARKER_ABORT (0x7F | FRAME_MARKER_ERROR_BIT) + +// HotSpot frame subtypes stored in a bitfield of the trace->lines[] +#define FRAME_HOTSPOT_STUB 0 +#define FRAME_HOTSPOT_VTABLE 1 +#define FRAME_HOTSPOT_INTERPRETER 2 +#define FRAME_HOTSPOT_NATIVE 3 + +#endif diff --git a/support/ebpf/hotspot_tracer.ebpf.c b/support/ebpf/hotspot_tracer.ebpf.c new file mode 100644 index 00000000..16413c6b --- /dev/null +++ b/support/ebpf/hotspot_tracer.ebpf.c @@ -0,0 +1,866 @@ +// This file contains the code and map definitions for the Java Hotspot VM tracer +// +// Much of the code principles are derived from the Java's DTrace plugin: +// https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/java.base/solaris/native/libjvm_db/libjvm_db.c +// See also the host agent interpreterjvm.go for more references. + +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" +#include "tracemgmt.h" +#include "types.h" +#include "errors.h" + +// Information extracted from a JDK `CodeBlob` instance. +typedef struct CodeBlobInfo { + // The start address of the CodeBlob. + u64 address; + // Value of the `CodeBlob::_code_start` field. + u64 code_start; + // Value of the `CodeBlob::_code_end` field. + u64 code_end; + // Value of the `CompiledMethod::deopt_handler` field. + // Only contains valid data if the CodeBlob is of `nmethod` or `CompiledMethod` type. + u64 deopt_handler; + // Determines the frame type. First 4 bytes of the string pointed to by `CodeBlob::_name`. + u32 frame_type; + // Value of the `nmethod::orig_pc_offset` field. + // Only contains valid data if this CodeBlob is of `nmethod` type. + u32 orig_pc_offset; + // Value of the `CodeBlob::_frame_size` field. + u32 frame_size; + // Value of the `CodeBlob::_frame_complete_offset` field. + u32 frame_comp; + // Value of the `nmethod::compile_id` field. + // Only contains valid data if this CodeBlob is of `nmethod` type. + u32 compile_id; +} CodeBlobInfo; + +// Context structure for information shared between all handlers in the HotSpot unwinder. +typedef struct HotspotUnwindInfo { + u64 sp; + u64 pc; + u64 fp; + // The value reported as the `file` field of the trace. + u64 file; + // The value reported as the `line` field of the trace. + struct { + // Subtype of the frame (JIT, interpreter). + u8 subtype; + // Either the delta between the code start and current PC (for compiled code) or the + // bytecode index (for interpreted code). + u32 pc_delta_or_bci; + // Validation cookie for the stored pointer. + // The value used here depends on the frame type. + u32 ptr_check; + } line; +} HotspotUnwindInfo; + +// Returned by frame type handlers to decide how this frame should be unwound. +typedef enum HotspotUnwindAction { + UA_UNWIND_INVALID, +#if defined(__aarch64__) + UA_UNWIND_AARCH64_LR, +#endif + UA_UNWIND_PC_ONLY, + UA_UNWIND_FRAME_POINTER, + UA_UNWIND_FP_PC, + UA_UNWIND_FRAME, + UA_UNWIND_REGS, + UA_UNWIND_COMPLETE, +} HotspotUnwindAction; + +// The number of hotspot frames to unwind per frame-unwinding eBPF program. +#define HOTSPOT_FRAMES_PER_PROGRAM 4 + +// The maximum number of HotSpot segmap lookup iterations. This is directly proportional +// to the size of JIT method code size. The longest sequence seen so far is from JDK8, +// and is 9 iterations. Include few extras. +#define HOTSPOT_SEGMAP_ITERATIONS 12 + +// The maximum number of JVM frame entries to search for a return address. In certain +// cases the JIT emits extra entries on the stack, and this controls the heuristic on +// how many extra entries are looked at. As reference the JVM async-profiler has similar +// heuristic and uses 7 slots on x86_64 (no search needed on aarch64). +#if defined(__x86_64__) +#define HOTSPOT_RA_SEARCH_SLOTS 6 +#endif + +// The hotspot frame type is distinguished from the first 4 characters of the CodeBlob +// type name. This provides constants for the needed strings. +#define FRAMETYPE_nmethod 0x74656d6e // "nmethod" +#define FRAMETYPE_native_nmethod 0x6974616e // "native nmethod" +#define FRAMETYPE_Interpreter 0x65746e49 // "Interpreter" +#define FRAMETYPE_vtable_chunks 0x62617476 // "vtable chunks" + +bpf_map_def SEC("maps") hotspot_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(HotspotProcInfo), + // This is the maximum number of JVM processes. Few machines should ever exceed 256 simultaneous + // JVMs running. Increase this value if 256 turns out to be insufficient. + .max_entries = 256, +}; + +// 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); +} + +// calc_line merges the three values to be encoded in a frame 'line' +static inline __attribute__((__always_inline__)) +u64 calc_line(u8 subtype, u32 pc_or_bci, u32 ptr_check) { + return ((u64)subtype << 60) | ((u64)pc_or_bci << 32) | (u64)ptr_check; +} + +// hotspot_addr_in_codecache checks if given address belongs to the JVM JIT code cache +__attribute__((always_inline)) inline static +bool hotspot_addr_in_codecache(u32 pid, u64 addr) { + PIDPage key = {}; + key.prefixLen = BIT_WIDTH_PID + BIT_WIDTH_PAGE; + key.pid = __constant_cpu_to_be32(pid); + key.page = __constant_cpu_to_be64(addr); + + // Check if we have the data for this virtual address + PIDPageMappingInfo* val = bpf_map_lookup_elem(&pid_page_to_mapping_info, &key); + if (!val) { + return false; + } + + // The address is valid only if it is hotspot unwindable code. + int program; + u64 bias; + decode_bias_and_unwind_program(val->bias_and_unwind_program, &bias, &program); + return program == PROG_UNWIND_HOTSPOT; +} + +// hotspot_find_codeblob maps a given PC to the CodeBlob* that describes the +// JIT information regarding the method (or stub) this PC belongs to. This uses +// information from the PidPageMapping for the PC. +static inline __attribute__((__always_inline__)) +u64 hotspot_find_codeblob(const UnwindState *state, const HotspotProcInfo *ji) +{ + unsigned long segment, codeblob, segmap_start; + u8 tag; + + DEBUG_PRINT("jvm: -> %lx in code start %lx, offset %lx", + (unsigned long) state->pc, (unsigned long) state->text_section_bias, (unsigned long) state->text_section_offset); + + // The segment map contains information on finding the control data + // structures given a PC. For documentation on this structure, see: + // https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/hotspot/share/memory/heap.cpp#l376 + + // Search for the code blob start using segmap. Hostagent will setup the mapping + // so that bias is the code segment start, and thus text_section_offset will hold + // the delta from start of the segment. It is shifted to get segment number. + segment = state->text_section_offset >> ji->segment_shift; + + // Segment map start is put in to the PidPageMapping's file_id. + segmap_start = (state->text_section_id >> HS_TSID_SEG_MAP_BIT) & HS_TSID_SEG_MAP_MASK; + +#pragma unroll + for (int i = 0; i < HOTSPOT_SEGMAP_ITERATIONS; i++) { + if (bpf_probe_read(&tag, sizeof(tag), (void*)(segmap_start + segment))) { + return 0; + } + DEBUG_PRINT("jvm: segment %lu, tag %u", segment, (unsigned) tag); + + // Stop if done or the segment is marked free + if (tag == 0 || tag == 0xff) { + break; + } + segment -= tag; + } + + if (tag != 0) { + // fail if we did not finish successfully + return 0; + } + + codeblob = state->text_section_bias + (segment << ji->segment_shift) + ji->heapblock_size; + + // We could check the HeapBlock::Header.used field, and possibly others + // for further validation of still valid block. + DEBUG_PRINT("jvm: -> mapped to codeblob %lx", codeblob); + return codeblob; +} + +__attribute__((always_inline)) inline static +ErrorCode hotspot_handle_vtable_chunks(HotspotUnwindInfo *ui, + HotspotUnwindAction *action) { + DEBUG_PRINT("jvm: -> unwind vtable"); + ui->line.subtype = FRAME_HOTSPOT_VTABLE; + +#if defined(__x86_64__) + // On x86 this has only the return address on stack. Code adapted from JDK-8178287. + // This is something JVM itself does not handle right. + *action = UA_UNWIND_PC_ONLY; +#elif defined(__aarch64__) + // On ARM64, nothing is put on stack for this at all. Unwind via LR. + *action = UA_UNWIND_AARCH64_LR; +#endif + + return ERR_OK; +} + +__attribute__((always_inline)) inline static +ErrorCode hotspot_handle_interpreter(UnwindState *state,Trace *trace, + HotspotUnwindInfo *ui, HotspotProcInfo *ji, + HotspotUnwindAction *action) { + // Hotspot Interpreter has it's custom stack layout, and the unwinding is done based + // on frame pointer. No frame information is in the CodeBlob header. + // The Interpreter internal offsets seem relatively stable, but would need to be programmed + // based on JVM version as they are not included in the introspection data. + if (ui->fp < ui->sp || ui->fp >= ui->sp + 0x1000) { + DEBUG_PRINT("jvm: fp too far away to be interpreter frame"); + goto error; + } + + // Read the Interpreter stack frame registers +#define FP_OFFS 10 +#if defined(__x86_64__) +// https://hg.openjdk.org/jdk-updates/jdk14u/file/default/src/hotspot/cpu/x86/frame_x86.hpp#l77 +#define BCP_SLOT_JVM9 8 +// https://github.com/openjdk/jdk8u/blob/master/hotspot/src/cpu/x86/vm/frame_x86.hpp#L117 +#define BCP_SLOT_JVM8 7 +// https://hg.openjdk.org/jdk-updates/jdk14u/file/default/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp#l66 +#define BCP_REGISTER r13 +#elif defined(__aarch64__) +// https://hg.openjdk.org/jdk-updates/jdk14u/file/default/src/hotspot/cpu/aarch64/frame_aarch64.hpp#l88 +#define BCP_SLOT_JVM9 9 +// https://github.com/openjdk/jdk8u/blob/master/hotspot/src/cpu/aarch64/vm/frame_aarch64.hpp#L125 +#define BCP_SLOT_JVM8 7 +// https://hg.openjdk.org/jdk-updates/jdk14u/file/default/src/hotspot/cpu/aarch64/assembler_aarch64.hpp#l136 +#define BCP_REGISTER r22 +#endif + u64 regs[FP_OFFS+2]; + if (bpf_probe_read(regs, sizeof(regs), (void *) (ui->fp - sizeof(u64[FP_OFFS])))) { + DEBUG_PRINT("jvm: failed to read interpreter frame"); + goto error; + } + + u64 bcp; + if (trace->stack_len) { + // Interpreter frame has the BCP value stored + if (ji->jvm_version >= 9) { + // JDK9+ frame has new 'mirror' slot which offsets the BCP slot by one + bcp = regs[FP_OFFS - BCP_SLOT_JVM9]; + } else { + // JDK8 and earlier + bcp = regs[FP_OFFS - BCP_SLOT_JVM8]; + } + } else { + // When Interpreter frame code is interrupted, the real BCP is kept in + // a register for performance. On x86_64 ABI it's on r13. + bcp = state->BCP_REGISTER; + } + + // Extract information from the frame + u64 method = regs[FP_OFFS - 3]; + ui->sp = regs[FP_OFFS - 1]; + ui->fp = regs[FP_OFFS]; + ui->pc = regs[FP_OFFS + 1]; + + // Convert Byte Code Pointer (BCP) to Byte Code Index (BCI); that is, convert the pointer to + // 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))) { + DEBUG_PRINT("jvm: failed to read interpreter cmethod"); + goto error; + } + if (bcp >= cmethod + ji->cmethod_size) { + // Convert Code Pointer to Index (offset) + bcp -= cmethod + ji->cmethod_size; + } + DEBUG_PRINT("jvm: -> method = 0x%lx, cmethod = 0x%lx, bcp = %lx", + (unsigned long) method, (unsigned long) cmethod, (unsigned long) bcp); + if (bcp >= 0xffff) { + // Range check, and mark BCI invalid if outside JVM spec range + bcp = 0xffff; + } + + // Interpreted frames send different pointers to host agent than other frame types. + ui->file = method; + ui->line.subtype = FRAME_HOTSPOT_INTERPRETER; + ui->line.pc_delta_or_bci = bcp; + ui->line.ptr_check = cmethod >> 3; + + *action = UA_UNWIND_COMPLETE; + return ERR_OK; + +error: + increment_metric(metricID_UnwindHotspotErrInterpreterFP); + return ERR_HOTSPOT_INTERPRETER_FP; +} + +#if defined(__x86_64__) +__attribute__((always_inline)) inline static +void breadcrumb_fixup(HotspotUnwindInfo *ui) { + // Nothing to do: breadcrumbs are not a thing on X86. +} +#elif defined(__aarch64__) +__attribute__((always_inline)) inline static +void breadcrumb_fixup(HotspotUnwindInfo *ui) { + // On ARM64, for some calls, the JVM pushes "breadcrumbs" onto the stack to make unwinding + // easier for them. In the process, they unfortunately make it harder for us, since we have + // to detect these cases and fix up SP accordingly. Fortunately, the code-gen is very static, + // so it is easy to detect. + // + // The inserted code looks like this: + // + // adr x9, ret_label + // lea x8, RuntimeAddress(entry) ;; pseudo instruction, expands to series of mov/movk insns + // stp zr, r11, [sp, #-16]! + // blr x8 + // ret_label: + // add sp, sp, 16 + // + // Note: x8 and x9 are JVM reserved scratch registers. + // + // The actual code generating this lives here: + // 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))); + if (lookback == 0xd63f0100a9bf27ffULL /* stp; blr */) { + ui->sp += 0x10; + } +} +#endif + +#if defined(__x86_64__) +__attribute__((always_inline)) inline static +ErrorCode hotspot_handle_prologue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, + HotspotUnwindAction *action) { + // In the prologue code. It generally consists of stack 'banging' (check for stack + // overflow), pushing FP, and finally allocating rest of the stack of 'frame_size'. + if (ui->pc >= cbi->code_start + cbi->frame_comp - 4) { + // Almost complete frame. Assume FP and PC on stack, and it's only the + // final stack allocation opcodes to be executed (add sp). + // TODO(tteras): This check is incomplete. There is some nasty variations + // which require looking at the prologue opcodes. + DEBUG_PRINT("jvm: -> unwinding incomplete frame (fp+pc)"); + *action = UA_UNWIND_FP_PC; + return ERR_OK; + } + // early in the prologue. assume only return address on stack + DEBUG_PRINT("jvm: -> unwinding incomplete frame (pc)"); + *action = UA_UNWIND_PC_ONLY; + return ERR_OK; +} +#elif defined(__aarch64__) +__attribute__((always_inline)) inline static +ErrorCode hotspot_handle_prologue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, + HotspotUnwindAction *action) { + // On ARM64, the prologue consists of various assembly snippets, most of which we aren't really + // concerned with. This includes stuff like stack banging (which, other than the name might + // suggest, doesn't actually write SP directly), initializing SVE registers and similar setup + // stuff. It ends with instructions generated according to the following pseudo-code: + // + // >>> if frame_size < (1 << 9) + 16: + // >>> sub sp, sp, frame_size + // >>> stp fp, lr, [sp, frame_size - 16] + // >>> if jdk_option_enabled(PreserveFramePointer): + // >>> add fp, sp, frame_size - 16 + // >>> else: + // >>> stp fp, lr, [sp, -16]! + // >>> if jdk_option_enabled(PreserveFramePointer): + // >>> mov fp, sp + // >>> if frame_size < (1 << 12) + 16: + // >>> sub sp, sp, frame_size - 16 + // >>> else: + // >>> # Note: x8 is reserved as a scratch register + // >>> mov x8, frame_size - 16 + // >>> sub sp, sp, x8 + // + // This general logic lives in the aarch64 variant of `MachPrologNode::emit`: + // https://github.com/openjdk/jdk/blob/jdk-17%2B35/src/hotspot/cpu/aarch64/aarch64.ad#L1883 + // The part that we care about resides in `MacroAssembler::build_frame`: + // https://github.com/openjdk/jdk/blob/jdk-17%2B35/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp#L4445 + // + // Frame sizes larger than (1 << 9) are exceedingly rare, so in practice, pretty much all + // prologues end like this (assuming `PreserveFramePointer` isn't being used): + // + // >>> sub sp, sp, frame_size + // >>> stp fp, lr, [sp, frame_size - 16] + // + // To unwind this prologue, all we need to do is to check whether the `sub` has already been + // executed, and, if it was, to fix up the stack pointer accordingly. After that, we simply + // unwind via the return address in the LR register. + + // Is the PC on the `stp` instruction? + if (ui->pc == cbi->code_start + cbi->frame_comp - 4) { + ui->sp += cbi->frame_size; + } + + *action = UA_UNWIND_AARCH64_LR; + return ERR_OK; +} +#endif + +#if defined(__x86_64__) +__attribute__((always_inline)) inline static +bool hotspot_handle_epilogue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, + HotspotUnwindAction *action) { + // On X86, epilogue handling is currently not implemented. + return false; +} +#elif defined(__aarch64__) +__attribute__((always_inline)) inline static +bool hotspot_handle_epilogue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, + HotspotUnwindAction *action) { + // On ARM64, the epilogue code is generated roughly like this: + // + // >>> remove_frame: + // >>> if framesize < (1 << 9) + 16: + // >>> ldp fp, lr, [sp, #(frame_size - 16)] + // >>> add sp, sp, frame_size + // >>> elif frame_size < (1 << 12) + 16: + // >>> add sp, sp, (frame_size - 16) + // >>> ldp fp, lr, [sp, #16]! + // >>> else: + // >>> mov rN, frame_size - 16 + // >>> add sp, sp, rN + // >>> ldp fp, lr, [sp, #16]! + // >>> safepoint_poll: + // >>> ldr x8, [x28, ] + // >>> cmp sp, x8 + // >>> b.hi + // >>> generated by unknown code: + // >>> ret + // + // In Java, it is extremely hard to create a function with a frame size larger than a few words. + // Handling the cases for the larger stack sizes is not really worth the instructions it would + // take up in the eBPF binary. The code below thus only handles the case where the frame size is + // smaller than `(1 << 9) + 16`. + + if (cbi->frame_size >= (1 << 9) + 16) { + // Frame sizes larger than this are extremely rare: skip these for now. + increment_metric(metricID_UnwindHotspotUnsupportedFrameSize); + return false; + } + + // Determine the search pattern for the epilogue begin of this function by assembling the aarch64 + // instructions that we expect the JRE to generate for the epilogue. + + // Encode `ldp fp, lr, [sp, #(frame_size - 16)]`. The OR inserts the immediate. + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDP--Load-Pair-of-Registers- + u64 ldp = 0xa9407bfd | ((((u64)cbi->frame_size - 16) / 8) << 15); + + // Encode `add sp, sp, frame_size`. The OR again places the immediate. + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADD--immediate---Add--immediate-- + u64 add = 0x910003ff | ((u64)cbi->frame_size << 10); + +#define EPI_LOOKBACK 6 +#define INSN_LEN 4 + + // Scan for the epilogue pattern, using a 64-bit wide sliding window with a 32-bit stride. + 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)); + +#pragma unroll + for (; find_offset < EPI_LOOKBACK - 1; ++find_offset) { + if (*(u64*)&window[find_offset] == needle) { + goto pattern_found; + } + } + + // Still here? Pattern not found, give up. + return false; + +pattern_found:; + + // Index Epilogue code Action to take when PC on instruction + // ----- ------------- ------------------------------------- + // 0 ldp fp, lr, [sp, #(frame_size - 16)] Bail out and let other code handle this case. + // 1 add sp, sp, frame_size Fix SP, then LR based unwinding. + // 2 ldr x8, [x28, ] LR based unwinding. + // 3 cmp sp, x8 LR based unwinding. + // 4 b.hi LR based unwinding. + // 5 ret LR based unwinding. + // + // When we find the ldp/add pattern in our look-back window, it thus means that we need to perform + // LR based unwinding. Since the look-back window ends at PC, the previous pattern search will not + // find the pattern and have bailed out when the PC is on the `ldp`, which implicitly handles the + // unwind action for the `ldp`. + + // If we're on the `add sp, sp, frame_size`, we need to fix up SP. The -1 is because the pattern + // is two instructions wide. + u8 epi_idx = EPI_LOOKBACK - 1 - find_offset; + if (epi_idx == 1) { + ui->sp += cbi->frame_size; + } + + DEBUG_PRINT("jvm: epilogue case"); + *action = UA_UNWIND_AARCH64_LR; + return true; + +#undef INSN_LEN +#undef EPI_LOOKBACK +} +#endif + +__attribute__((always_inline)) inline static +ErrorCode hotspot_handle_nmethod(const CodeBlobInfo *cbi, Trace *trace, + HotspotUnwindInfo *ui, HotspotProcInfo *ji, + HotspotUnwindAction *action) { + // setup frame subtype, and get the native method _compile_id as pointer cookie + // as it is unique to the compilation result + + ui->line.subtype = FRAME_HOTSPOT_NATIVE; + ui->line.ptr_check = cbi->compile_id; + + u64 deopt_handler = cbi->deopt_handler; + if (ji->jvm_version <= 8) { + // JDK7/8: Deoptimization handler is an uint32 offset from the code blob start + deopt_handler = cbi->address + (deopt_handler & 0xffffffff); + } + if (ui->pc == deopt_handler) { + // If the PC where execution is to continue is the deoptimization handler, the frame + // has been deoptimized. This happens when something happened in the upper frames, + // that broke the assumptions used at JIT compile time. + // In practice the JVM rewrote the return address at the callers frame. It also stores + // original PC before rewriting. This code retrieves that. For the deoptimization handler + // generation look at: + // https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp#l2906 + // 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)) || + 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. + DEBUG_PRINT("jvm: -> deoptimized frame, pc recovery failed"); + } else { + DEBUG_PRINT("jvm: -> deoptimized frame, pc recovered as 0x%lx (from sp+%d)", (unsigned long) orig, + (s32) cbi->orig_pc_offset); + ui->pc = orig; + ui->line.pc_delta_or_bci = ui->pc - cbi->code_start; + } + } + + // Are we in the prologue? + if (ui->pc < cbi->code_start + cbi->frame_comp) { + return hotspot_handle_prologue(cbi, ui, action); + } + + // Attempt prologue unwinding. + if (hotspot_handle_epilogue(cbi, ui, action)) { + return ERR_OK; + } + + if (ui->fp >= ui->sp && ui->fp < ui->sp + cbi->frame_size + sizeof(u64[6])) { + // FP is in a "sane" range for a frame-pointer based function: + // Between SP and SP+frame_size+few extra words. + // That is, FP points to valid stack position that could be the frame. If it FP was used + // as a general-purpose register, it would likely be something outside this range. + // The native functions always store FP. It is valid frame pointer if this is the topmost + // native frame after Interpreter, or always with -XX:+PreserveFramePointer. + // NOTE: some other instances used frame_size * 2, but that can cause false positives when + // frame_size is large. The FP would look valid, but if using it, we'd be actually jumping + // over one or more stack frames. This happens when none of the function in between modify + // FP. Also, if we skipped the functions, we would not be able to restore FP from + // the skipped frames and potentially cause the whole unwinding to fail in later stage. + DEBUG_PRINT("jvm: -> using frame pointer (frame size %ld)", (long) (ui->fp - ui->sp)); + *action = UA_UNWIND_FRAME_POINTER; + return ERR_OK; + } + // The real JVM has the same limitation. async-profiler has some heuristic examples for this. + + breadcrumb_fixup(ui); + + // Assume complete frame without frame pointer, use the CodeBlob frame_size. + ui->sp += cbi->frame_size; + +#ifndef HOTSPOT_RA_SEARCH_SLOTS + // Frame size can be trusted. + *action = UA_UNWIND_REGS; + return ERR_OK; +#else + // On x86, the generated code can occasionally push extra words to the stack and it might + // be more than the advertised `frame_size`. The official unwinder seems to not handle this + // case properly. This follows the Hotspot frame::safe_for_sender and async-profiler heuristic + // to assume that PC points to valid code location inside the CodeCache. This is true for all + // native methods as they are always called by another native method or a stub. + // + // For EBPF simplicity, this just verifies that the PC address is inside the active memory + // 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))); + 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])) { + DEBUG_PRINT("jvm: -> unwinding complete frame + %d words", i); + *action = UA_UNWIND_REGS; + return ERR_OK; + } + } + increment_metric(metricID_UnwindHotspotErrInvalidRA); + return ERR_HOTSPOT_INVALID_RA; +#endif +} + +__attribute__((always_inline)) inline static +ErrorCode hotspot_handle_stub_fallback(const CodeBlobInfo *cbi, + HotspotUnwindAction *action) { + DEBUG_PRINT("jvm: -> unwind stub fallback path"); + + if (!cbi->frame_size) { + // "StubRoutines (1)" and "StubRoutines (2)" will have zero frame_size, + // but valid frame pointer. + *action = UA_UNWIND_FRAME_POINTER; + return ERR_OK; + } + + *action = UA_UNWIND_FRAME; + return ERR_OK; +} + +__attribute__((always_inline)) inline static +ErrorCode hotspot_handle_stub(const UnwindState *state, const CodeBlobInfo *cbi, + HotspotUnwindInfo *ui, HotspotUnwindAction *action) { + ui->line.subtype = FRAME_HOTSPOT_STUB; + +#ifdef __aarch64__ + u64 info = state->text_section_id; + if (!(info & (1UL << HS_TSID_IS_STUB_BIT))) { + return hotspot_handle_stub_fallback(cbi, action); + } + + DEBUG_PRINT("jvm: -> unwind stub with unwind info 0x%016llX", info); + + if (info & (1UL << HS_TSID_HAS_FRAME_BIT)) { + *action = UA_UNWIND_FRAME_POINTER; + return ERR_OK; + } + + u64 delta = (info >> HS_TSID_STACK_DELTA_BIT); + delta &= HS_TSID_STACK_DELTA_MASK; + delta *= HS_TSID_STACK_DELTA_SCALE; + + ui->sp += delta; + + *action = UA_UNWIND_AARCH64_LR; + return ERR_OK; +#else + return hotspot_handle_stub_fallback(cbi, action); +#endif +} + +__attribute__((always_inline)) inline static +ErrorCode hotspot_execute_unwind_action(CodeBlobInfo *cbi, HotspotUnwindAction action, + HotspotUnwindInfo *ui, UnwindState *state, Trace *trace) { + switch (action) { + case UA_UNWIND_INVALID: + return ERR_UNREACHABLE; +#if defined(__aarch64__) + case UA_UNWIND_AARCH64_LR: + if (!state->lr_valid) { + increment_metric(metricID_UnwindHotspotErrLrUnwindingMidTrace); + return ERR_HOTSPOT_LR_UNWINDING_MID_TRACE; + } + ui->pc = state->lr; + goto unwind_complete; +#endif + case UA_UNWIND_PC_ONLY: + cbi->frame_size = sizeof(u64); + goto unwind_frame; + case UA_UNWIND_FRAME_POINTER: + ui->sp = ui->fp; + // fallthrough + case UA_UNWIND_FP_PC: + cbi->frame_size = sizeof(u64[2]); + // fallthrough + case UA_UNWIND_FRAME: + unwind_frame: + ui->sp += cbi->frame_size; + // fallthrough + case UA_UNWIND_REGS: { + u64 frame[2]; + bpf_probe_read(frame, sizeof(frame), (void *) (ui->sp - sizeof(frame))); + ui->pc = frame[1]; + if (cbi->frame_size >= sizeof(frame)) { + DEBUG_PRINT("jvm: -> recover fp"); + ui->fp = frame[0]; + } + } // fallthrough + 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); + if (error) { + return error; + } + + DEBUG_PRINT("jvm: -> pc: %lx, sp: %lx, fp: %lx", + (unsigned long) ui->pc, (unsigned long) ui->sp, (unsigned long) ui->fp); + state->pc = ui->pc; + state->sp = ui->sp; + state->fp = ui->fp; +#if defined(__aarch64__) + state->lr_valid = false; +#endif + increment_metric(metricID_UnwindHotspotFrames); + } + } + + return ERR_OK; +} + +// Reads information from the CodeBlob for the current PC location from the JVM process. +__attribute__((always_inline)) inline static +ErrorCode hotspot_read_codeblob(const UnwindState *state, const HotspotProcInfo *ji, + HotspotUnwindScratchSpace *scratch, CodeBlobInfo *cbi) { + // Find the CodeBlob (JIT function metadata) for this PC. + cbi->address = hotspot_find_codeblob(state, ji); + if (!cbi->address) { + DEBUG_PRINT("jvm: no codeblob matched for pc"); + increment_metric(metricID_UnwindHotspotErrNoCodeblob); + return ERR_HOTSPOT_NO_CODEBLOB; + } + + // Read the CodeBlob. Note that this is intentionally a memory over-read in most cases: we read + // the entire size of our CodeBlob buffer despite the CodeBlob typically being smaller than that + // buffer. This way, we don't have to do a second read for the frame type in order to determine + // 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)) { + goto read_error_exit; + } + + // Make the verifier happy. No bound checks required for the remaining offsets: they are u8, and + // the verifier is aware that their maximum value is smaller than our `codeblob` buffer. + if (ji->compiledmethod_deopt_handler + sizeof(u64) > sizeof(scratch->codeblob) || + ji->nmethod_compileid + sizeof(u32) > sizeof(scratch->codeblob) || + ji->nmethod_orig_pc_offset + sizeof(u64) > sizeof(scratch->codeblob)) { + return ERR_UNREACHABLE; + } + + // Extract the needed CodeBlob fields. + cbi->code_start = *(u64*)(scratch->codeblob + ji->codeblob_codestart); + cbi->code_end = *(u64*)(scratch->codeblob + ji->codeblob_codeend); + cbi->frame_size = *(u32*)(scratch->codeblob + ji->codeblob_framesize) * 8; + cbi->frame_comp = *(u32*)(scratch->codeblob + ji->codeblob_framecomplete); + cbi->compile_id = *(u32*)(scratch->codeblob + ji->nmethod_compileid); + cbi->orig_pc_offset = *(u32*)(scratch->codeblob + ji->nmethod_orig_pc_offset); + cbi->deopt_handler = *(u64*)(scratch->codeblob + ji->compiledmethod_deopt_handler); + + // `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)) { + goto read_error_exit; + } + + if (ji->jvm_version <= 8) { + // JDK7/8: Code start and end are actually uint32 offsets from the code blob start + cbi->code_start = cbi->address + (cbi->code_start & 0xffffffff); + cbi->code_end = cbi->address + (cbi->code_end & 0xffffffff); + } + + DEBUG_PRINT("jvm: -> code %lx-%lx", + (unsigned long)cbi->code_start, (unsigned long)cbi->code_end); + DEBUG_PRINT("jvm: -> frame_complete %u, frame_size %u, frame_type 0x%x", + cbi->frame_comp, cbi->frame_size, cbi->frame_type); + + return 0; + +read_error_exit: + DEBUG_PRINT("jvm: failed to read codeblob"); + increment_metric(metricID_UnwindHotspotErrInvalidCodeblob); + return ERR_HOTSPOT_INVALID_CODEBLOB; +} + +// hotspot_unwind_one_frame fully unwinds one HotSpot frame +static ErrorCode hotspot_unwind_one_frame(PerCPURecord *record, HotspotProcInfo *ji) { + UnwindState *state = &record->state; + Trace *trace = &record->trace; + HotspotUnwindInfo ui; + + increment_metric(metricID_UnwindHotspotAttempts); + + ui.pc = state->pc; + ui.sp = state->sp; + ui.fp = state->fp; + + // Read the CodeBlob. + CodeBlobInfo cbi; + ErrorCode err = hotspot_read_codeblob(state, ji, &record->hotspotUnwindScratch, &cbi); + if (err) { + return err; + } + + // For most frame types, the CodeBlob address also serves as the file. + ui.file = cbi.address; + ui.line.ptr_check = cbi.frame_type; + ui.line.pc_delta_or_bci = ui.pc - cbi.code_start; + + HotspotUnwindAction action = UA_UNWIND_INVALID; + switch (cbi.frame_type) { + case FRAMETYPE_nmethod: // JIT-compiled method + case FRAMETYPE_native_nmethod: // stub to call C-implemented java method + err = hotspot_handle_nmethod(&cbi, trace, &ui, ji, &action); + break; + case FRAMETYPE_Interpreter: // main Interpreter program running byte code + err = hotspot_handle_interpreter(state, trace, &ui, ji, &action); + break; + case FRAMETYPE_vtable_chunks: // megamorphic interface call site + err = hotspot_handle_vtable_chunks(&ui, &action); + break; + default: // stubs and intrinsic functions (too many to list) + err = hotspot_handle_stub(state, &cbi, &ui, &action); + } + + if (err) { + return err; + } + + return hotspot_execute_unwind_action(&cbi, action, &ui, state, trace); +} + +// unwind_hotspot is the entry point for tracing when invoked from the native tracer +// and it recursive unwinds all HotSpot frames and then jumps back to unwind further +// native frames that follow. +SEC("perf_event/unwind_hotspot") +int unwind_hotspot(struct pt_regs *ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + Trace *trace = &record->trace; + pid_t pid = trace->pid; + DEBUG_PRINT("==== jvm: unwind %d ====", trace->stack_len); + + HotspotProcInfo *ji = bpf_map_lookup_elem(&hotspot_procs, &pid); + if (!ji) { + DEBUG_PRINT("jvm: no HotspotProcInfo for this pid"); + return 0; + } + + int unwinder = PROG_UNWIND_STOP; + ErrorCode error = ERR_OK; +#pragma unroll + for (int i = 0; i < HOTSPOT_FRAMES_PER_PROGRAM; i++) { + unwinder = PROG_UNWIND_STOP; + error = hotspot_unwind_one_frame(record, ji); + if (error) { + break; + } + + error = get_next_unwinder_after_native_frame(record, &unwinder); + if (error || unwinder != PROG_UNWIND_HOTSPOT) { + break; + } + } + + record->state.unwind_error = error; + tail_call(ctx, unwinder); + DEBUG_PRINT("jvm: tail call for next frame unwinder (%d) failed", unwinder); + return -1; +} diff --git a/support/ebpf/integration_test.ebpf.c b/support/ebpf/integration_test.ebpf.c new file mode 100644 index 00000000..3e85e33d --- /dev/null +++ b/support/ebpf/integration_test.ebpf.c @@ -0,0 +1,35 @@ +// This file contains the code and map definitions that are used in integration tests only. + +#include "bpfdefs.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, +}; + + +// 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; + } + + return 0; +} diff --git a/support/ebpf/interpreter_dispatcher.ebpf.c b/support/ebpf/interpreter_dispatcher.ebpf.c new file mode 100644 index 00000000..dd903a53 --- /dev/null +++ b/support/ebpf/interpreter_dispatcher.ebpf.c @@ -0,0 +1,201 @@ +// This file contains the code and map definitions that are shared between +// the tracers, as well as a dispatcher program that can be attached to a +// perf event and will call the appropriate tracer for a given process + +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" +#include "types.h" +#include "tracemgmt.h" + +// Begin shared maps + +// Per-CPU record of the stack being built and meta-data on the building process +bpf_map_def SEC("maps") per_cpu_records = { + .type = BPF_MAP_TYPE_PERCPU_ARRAY, + .key_size = sizeof(int), + .value_size = sizeof(PerCPURecord), + .max_entries = 1, +}; + +// metrics maps metric ID to a value +bpf_map_def SEC("maps") metrics = { + .type = BPF_MAP_TYPE_PERCPU_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(u64), + .max_entries = metricID_Max, +}; + +// progs maps from a program ID to an eBPF program +bpf_map_def SEC("maps") progs = { + .type = BPF_MAP_TYPE_PROG_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(u32), + .max_entries = NUM_TRACER_PROGS, +}; + +// report_events notifies user space about events (GENERIC_PID and TRACES_FOR_SYMBOLIZATION). +// +// As a key the CPU number is used and the value represents a perf event file descriptor. +// Information transmitted is the event type only. We use 0 as the number of max entries +// for this map as at load time it will be replaced by the number of possible CPUs. At +// the same time this will then also define the number of perf event rings that are +// used for this map. +bpf_map_def SEC("maps") report_events = { + .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, + .key_size = sizeof(int), + .value_size = sizeof(u32), + .max_entries = 0, +}; + +// 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. +bpf_map_def SEC("maps") reported_pids = { + .type = BPF_MAP_TYPE_LRU_HASH, + .key_size = sizeof(u32), + .value_size = sizeof(u64), + .max_entries = 65536, +}; + +// pid_events is a map that holds PIDs that should be processed in user space. +// +// User space code will periodically iterate through the map and process each entry. +// Additionally, each time eBPF code writes a value into the map, user space is notified +// through event_send_trigger (which uses maps/report_events). As key we use the PID of +// the process and as value always true. When sizing this map, we are thinking about +// the maximum number of unique PIDs that could generate events we're interested in +// (process new, process exit, unknown PC) within a map monitor/processing interval, +// that we would like to support. +bpf_map_def SEC("maps") pid_events = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(u32), + .value_size = sizeof(bool), + .max_entries = 65536, +}; + + +// The native unwinder needs to be able to determine how each mapping should be unwound. +// +// This map contains data to help the native unwinder translate from a virtual address in a given +// process. It contains information of the unwinder program to use, how to convert the virtual +// address to relative address, and what executable file is in question. +bpf_map_def SEC("maps") pid_page_to_mapping_info = { + .type = BPF_MAP_TYPE_LPM_TRIE, + .key_size = sizeof(PIDPage), + .value_size = sizeof(PIDPageMappingInfo), + .max_entries = 524288, // 2^19 + .map_flags = BPF_F_NO_PREALLOC, +}; + +// inhibit_events map is used to inhibit sending events to user space. +// +// Only one event needs to be sent as it's a manual trigger to start processing +// traces / PIDs early. HA (Go) will reset this entry once it has reacted to the +// trigger, so next event is sent when needed. +// NOTE: Update .max_entries if additional event types are added. The value should +// equal the number of different event types using this mechanism. +bpf_map_def SEC("maps") inhibit_events = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(u32), + .value_size = sizeof(bool), + .max_entries = 2, +}; + +// Perf event ring buffer for sending completed traces to user-mode. +// +// The map is periodically polled and read from in `tracer`. +bpf_map_def SEC("maps") trace_events = { + .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, + .key_size = sizeof(int), + .value_size = 0, + .max_entries = 0, +}; + +// End shared maps + +SEC("perf_event/unwind_stop") +int unwind_stop(struct pt_regs *ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + Trace *trace = &record->trace; + UnwindState *state = &record->state; + + // 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) { + DEBUG_PRINT("unwind_stop called but the stack is empty"); + increment_metric(metricID_ErrEmptyStack); + if (!state->unwind_error) { + state->unwind_error = ERR_EMPTY_STACK; + } + } + + // If unwinding was aborted due to a critical error, push an error frame. + if (state->unwind_error) { + push_error(&record->trace, state->unwind_error); + } + + switch (state->error_metric) { + case -1: + // No Error + break; + case metricID_UnwindNativeErrWrongTextSection:; + if (report_pid(ctx, trace->pid, true)) { + increment_metric(metricID_NumUnknownPC); + } + // Fallthrough to report the error + default: + increment_metric(state->error_metric); + } + + // TEMPORARY HACK + // + // If we ended up with a trace that consists of only a single error frame, drop it. + // This is required as long as the process manager provides the option to filter out + // error frames, to prevent empty traces from being sent. While it might seem that this + // filtering should belong into the HA code that does the filtering, it is actually + // surprisingly hard to implement that way: since traces and their counts are reported + // through different data structures, we'd have to keep a list of known empty traces to + // also prevent the corresponding trace counts to be sent out. OTOH, if we do it here, + // this is trivial. + if (trace->stack_len == 1 && trace->kernel_stack_id < 0 && state->unwind_error) { + u32 syscfg_key = 0; + SystemConfig* syscfg = bpf_map_lookup_elem(&system_config, &syscfg_key); + if (!syscfg) { + return -1; // unreachable + } + + if (syscfg->drop_error_only_traces) { + return 0; + } + } + // TEMPORARY HACK END + + send_trace(ctx, trace); + + return 0; +} + +char _license[] SEC("license") = "GPL"; +// this number will be interpreted by the elf loader +// to set the current running kernel version +u32 _version SEC("version") = 0xFFFFFFFE; + +// tracepoint__sys_enter_read serves as dummy tracepoint so we can check if tracepoints are +// enabled and we can make use of them. +// The argument that is passed to the tracepoint for the sys_enter_read hook is described in sysfs +// at /sys/kernel/debug/tracing/events/syscalls/sys_enter_read/format. +SEC("tracepoint/syscalls/sys_enter_read") +int tracepoint__sys_enter_read(void *ctx) { + printt("The read tracepoint was triggered"); + return 0; +} diff --git a/support/ebpf/inttypes.h b/support/ebpf/inttypes.h new file mode 100644 index 00000000..cac49a1e --- /dev/null +++ b/support/ebpf/inttypes.h @@ -0,0 +1,23 @@ +#ifndef OPTI_INTTYPES_H +#define OPTI_INTTYPES_H + +// The kconfig header is required when performing actual eBPF builds but not +// even present in user-mode builds, so we have to make the include conditional. +#if defined(__KERNEL__) +# include +#endif + +#include + +// Some test targets (user-mode tests, integration tests) don't have these +// non-underscore types, so we make sure they exist here. +typedef __s8 s8; +typedef __u8 u8; +typedef __s16 s16; +typedef __u16 u16; +typedef __s32 s32; +typedef __u32 u32; +typedef __s64 s64; +typedef __u64 u64; + +#endif // OPTI_INTTYPES_H diff --git a/support/ebpf/native_stack_trace.ebpf.c b/support/ebpf/native_stack_trace.ebpf.c new file mode 100644 index 00000000..ee9b3f27 --- /dev/null +++ b/support/ebpf/native_stack_trace.ebpf.c @@ -0,0 +1,877 @@ +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" + +#ifdef TESTING_COREDUMP +// defined in include/uapi/linux/perf_event.h +#define PERF_MAX_STACK_DEPTH 127 + +#ifndef THREAD_SIZE + // taken from the kernel sources + #define THREAD_SIZE 16384 +#endif + +#else + #include + #include + #include + #include +#endif + +#include "frametypes.h" +#include "types.h" +#include "tracemgmt.h" +#include "stackdeltatypes.h" + +// 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) \ + bpf_map_def SEC("maps") exe_id_to_##X##_stack_deltas = { \ + .type = BPF_MAP_TYPE_HASH_OF_MAPS, \ + .key_size = sizeof(u64), \ + .value_size = sizeof(u32), \ + .max_entries = 4096, \ + }; + +// Create buckets to hold the stack delta information for the executables. +STACK_DELTA_BUCKET(8); +STACK_DELTA_BUCKET(9); +STACK_DELTA_BUCKET(10); +STACK_DELTA_BUCKET(11); +STACK_DELTA_BUCKET(12); +STACK_DELTA_BUCKET(13); +STACK_DELTA_BUCKET(14); +STACK_DELTA_BUCKET(15); +STACK_DELTA_BUCKET(16); +STACK_DELTA_BUCKET(17); +STACK_DELTA_BUCKET(18); +STACK_DELTA_BUCKET(19); +STACK_DELTA_BUCKET(20); +STACK_DELTA_BUCKET(21); + +// Unwind info value for invalid stack delta +#define STACK_DELTA_INVALID (STACK_DELTA_COMMAND_FLAG | UNWIND_COMMAND_INVALID) +#define STACK_DELTA_STOP (STACK_DELTA_COMMAND_FLAG | UNWIND_COMMAND_STOP) + +// An array of unwind info contains the all the different UnwindInfo instances +// needed system wide. Individual stack delta entries refer to this array. +bpf_map_def SEC("maps") unwind_info_array = { + .type = BPF_MAP_TYPE_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(UnwindInfo), + // Maximum number of unique stack deltas needed on a system. This is based on + // normal desktop /usr/bin/* and /usr/lib/*.so having about 9700 unique deltas. + // Can be increased up to 2^15, see also STACK_DELTA_COMMAND_FLAG. + .max_entries = 16384, +}; + +// The number of native frames to unwind per frame-unwinding eBPF program. +#define NATIVE_FRAMES_PER_PROGRAM 4 + +// The decision whether to unwind native stacks or interpreter stacks is made by checking if a given +// PC address falls into the "interpreter loop" of an interpreter. This map helps identify such +// loops: The keys are those executable section IDs that contain interpreter loops, the values +// identify the offset range within this executable section that contains the interpreter loop. +bpf_map_def SEC("maps") interpreter_offsets = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(u64), + .value_size = sizeof(OffsetRange), + .max_entries = 32, +}; + +// Maps fileID and page to information of stack deltas associated with that page. +bpf_map_def SEC("maps") stack_delta_page_to_info = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(StackDeltaPageKey), + .value_size = sizeof(StackDeltaPageInfo), + .max_entries = 40000, +}; + +// This contains the kernel PCs as returned by bpf_get_stackid(). Unfortunately the ebpf +// program cannot read the contents, so we return the stackid in the Trace directly, and +// make the profiling agent read the kernel mode stack trace portion from this map. +bpf_map_def SEC("maps") kernel_stackmap = { + .type = BPF_MAP_TYPE_STACK_TRACE, + .key_size = sizeof(u32), + .value_size = PERF_MAX_STACK_DEPTH * sizeof(u64), + .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); +} + +#ifdef __aarch64__ +// Strips the PAC tag from a pointer. +// +// While all pointers can contain PAC tags, we only apply this function to code pointers, because +// that's where normalization is required to make the stack delta lookups work. Note that if that +// should ever change, we'd need a different mask for the data pointers, because it might diverge +// from the mask for code pointers. +static inline u64 normalize_pac_ptr(u64 ptr) { + // Retrieve PAC mask from the system config. + u32 key = 0; + SystemConfig* syscfg = bpf_map_lookup_elem(&system_config, &key); + if (!syscfg) { + // Unreachable: array maps are always fully initialized. + return ptr; + } + + // Mask off PAC bits. Since we're always applying this to usermode pointers that should have all + // the high bits set to 0, we don't need to consider the case of having to fill up the resulting + // hole with 1s (like we'd have to for kernel ptrs). + ptr &= syscfg->inverse_pac_mask; + return ptr; +} +#endif + +// A single step for the bsearch into the big_stack_deltas array. This is really a textbook bsearch +// step, built in a way to update the value of *lo and *hi. This function will be called repeatedly +// (since we cannot do loops). The return value signals whether the bsearch came to an end / found +// the right element or whether it needs to continue. +static inline __attribute__((__always_inline__)) +bool bsearch_step(void* inner_map, u32* lo, u32* hi, u16 page_offset) { + u32 pivot = (*lo + *hi) >> 1; + StackDelta *delta = bpf_map_lookup_elem(inner_map, &pivot); + if (!delta) { + *hi = 0; + return false; + } + if (page_offset >= delta->addrLow) { + *lo = pivot + 1; + } else { + *hi = pivot; + } + return *lo < *hi; +} + +// Get the outer map based on the number of stack delta entries. +static inline __attribute__((__always_inline__)) +void *get_stack_delta_map(int mapID) { + switch (mapID) { + case 8: return &exe_id_to_8_stack_deltas; + case 9: return &exe_id_to_9_stack_deltas; + case 10: return &exe_id_to_10_stack_deltas; + case 11: return &exe_id_to_11_stack_deltas; + case 12: return &exe_id_to_12_stack_deltas; + case 13: return &exe_id_to_13_stack_deltas; + case 14: return &exe_id_to_14_stack_deltas; + case 15: return &exe_id_to_15_stack_deltas; + case 16: return &exe_id_to_16_stack_deltas; + case 17: return &exe_id_to_17_stack_deltas; + case 18: return &exe_id_to_18_stack_deltas; + case 19: return &exe_id_to_19_stack_deltas; + case 20: return &exe_id_to_20_stack_deltas; + case 21: return &exe_id_to_21_stack_deltas; + default: return NULL; + } +} + +// Get the stack offset of the given instruction. +static ErrorCode get_stack_delta(u64 text_section_id, u64 text_section_offset, + int* addrDiff, u32* unwindInfo) { + u64 exe_id = text_section_id; + + // Look up the stack delta page information for this address. + StackDeltaPageKey key = { }; + key.fileID = text_section_id; + key.page = text_section_offset & ~STACK_DELTA_PAGE_MASK; + DEBUG_PRINT("Look up stack delta for %lx:%lx", + (unsigned long)text_section_id, (unsigned long)text_section_offset); + StackDeltaPageInfo *info = bpf_map_lookup_elem(&stack_delta_page_to_info, &key); + if (!info) { + DEBUG_PRINT("Failure to look up stack delta page fileID %lx, page %lx", + (unsigned long)key.fileID, (unsigned long)key.page); + increment_metric(metricID_UnwindNativeErrLookupTextSection); + return ERR_NATIVE_LOOKUP_TEXT_SECTION; + } + + void *outer_map = get_stack_delta_map(info->mapID); + if (!outer_map) { + DEBUG_PRINT("Failure to look up outer map for text section %lx in mapID %d", + (unsigned long) exe_id, (int) info->mapID); + increment_metric(metricID_UnwindNativeErrLookupStackDeltaOuterMap); + return ERR_NATIVE_LOOKUP_STACK_DELTA_OUTER_MAP; + } + + void *inner_map = bpf_map_lookup_elem(outer_map, &exe_id); + if (!inner_map) { + DEBUG_PRINT("Failure to look up inner map for text section %lx", + (unsigned long) exe_id); + increment_metric(metricID_UnwindNativeErrLookupStackDeltaInnerMap); + return ERR_NATIVE_LOOKUP_STACK_DELTA_INNER_MAP; + } + + // Preinitialize the idx for the index to use for page without any deltas. + u32 idx = info->firstDelta; + u16 page_offset = text_section_offset & STACK_DELTA_PAGE_MASK; + if (info->numDeltas) { + // Page has deltas, so find the correct one to use using binary search. + u32 lo = info->firstDelta; + u32 hi = lo + info->numDeltas; + + DEBUG_PRINT("Intervals should be from %lu to %lu (mapID %d)", + (unsigned long) lo, (unsigned long) hi, (int)info->mapID); + + // Do the binary search, up to 16 iterations. Deltas are paged to 64kB pages. + // They can contain at most 64kB deltas even if everything is single byte opcodes. + int i; +#pragma unroll + for (i = 0; i < 16; i++) { + if (!bsearch_step(inner_map, &lo, &hi, page_offset)) { + break; + } + } + if (i >= 16 || hi == 0) { + DEBUG_PRINT("Failed bsearch in 16 steps. Corrupt data?"); + increment_metric(metricID_UnwindNativeErrLookupIterations); + return ERR_NATIVE_EXCEEDED_DELTA_LOOKUP_ITERATIONS; + } + // After bsearch, 'hi' points to the first entry greater than the requested. + idx = hi; + } + + // The code above found the first entry with greater address than requested, + // so it needs to be decremented by one to get the entry with equal-or-less. + // This makes also the logic work cross-pages: if the first entry in within + // the page is too large, this actually gets the entry from the previous page. + idx--; + + StackDelta *delta = bpf_map_lookup_elem(inner_map, &idx); + if (!delta) { + increment_metric(metricID_UnwindNativeErrLookupRange); + return ERR_NATIVE_LOOKUP_RANGE; + } + + DEBUG_PRINT("delta index %d, addrLow 0x%x, unwindInfo %d", + idx, delta->addrLow, delta->unwindInfo); + + // Calculate PC delta from stack delta for merged delta comparison + int deltaOffset = (int)page_offset - (int)delta->addrLow; + if (idx < info->firstDelta) { + // PC is below the first delta of the corresponding page. This means that + // delta->addrLow contains address relative to one page before the page_offset. + // Fix up the deltaOffset with this difference of base pages. + deltaOffset += 1 << STACK_DELTA_PAGE_BITS; + } + + *addrDiff = deltaOffset; + *unwindInfo = delta->unwindInfo; + + if (delta->unwindInfo == STACK_DELTA_INVALID) { + increment_metric(metricID_UnwindNativeErrStackDeltaInvalid); + return ERR_NATIVE_STACK_DELTA_INVALID; + } + if (delta->unwindInfo == STACK_DELTA_STOP) { + increment_metric(metricID_UnwindNativeStackDeltaStop); + } + + return ERR_OK; +} + +// unwind_register_address calculates the given expression ('opcode'/'param') to get +// the CFA (canonical frame address, to recover PC and be used in further calculations), +// or the address where a register is stored (FP currently), so that the value of +// the register can be recovered. +// +// Currently the following expressions are supported: +// 1. Not recoverable -> NULL is returned. +// 2. When UNWIND_OPCODEF_DEREF is not set: +// BASE + param +// 3. When UNWIND_OPCODEF_DEREF is set: +// *(BASE + preDeref) + postDeref +static inline __attribute__((__always_inline__)) +u64 unwind_register_address(UnwindState *state, u64 cfa, u8 opcode, s32 param) { + unsigned long addr, val; + s32 preDeref = param, postDeref = 0; + + if (opcode & UNWIND_OPCODEF_DEREF) { + // For expressions that dereference the base expression, the parameter is constructed + // of pre-dereference and post-derefence operands. Unpack those. + preDeref &= ~UNWIND_DEREF_MASK; + postDeref = (param & UNWIND_DEREF_MASK) * UNWIND_DEREF_MULTIPLIER; + } + + // Resolve the 'BASE' register, and fetch the CFA/FP/SP value. + switch (opcode & ~UNWIND_OPCODEF_DEREF) { + case UNWIND_OPCODE_BASE_CFA: + addr = cfa; + break; + case UNWIND_OPCODE_BASE_FP: + addr = state->fp; + break; + case UNWIND_OPCODE_BASE_SP: + addr = state->sp; + break; +#if defined(__aarch64__) + case UNWIND_OPCODE_BASE_LR: + DEBUG_PRINT("unwind: lr"); + + if (state->lr == 0) { + increment_metric(metricID_UnwindNativeLr0); + DEBUG_PRINT("Failure to unwind frame: zero LR at %llx", state->pc); + return 0; + } + + return state->lr; +#endif + default: + return 0; + } + +#ifdef OPTI_DEBUG + switch (opcode) { + case UNWIND_OPCODE_BASE_CFA: + DEBUG_PRINT("unwind: cfa+%d", preDeref); + break; + case UNWIND_OPCODE_BASE_FP: + DEBUG_PRINT("unwind: fp+%d", preDeref); + break; + case UNWIND_OPCODE_BASE_SP: + DEBUG_PRINT("unwind: sp+%d", preDeref); + break; + case UNWIND_OPCODE_BASE_CFA | UNWIND_OPCODEF_DEREF: + DEBUG_PRINT("unwind: *(cfa+%d)+%d", preDeref, postDeref); + break; + case UNWIND_OPCODE_BASE_FP | UNWIND_OPCODEF_DEREF: + DEBUG_PRINT("unwind: *(fp+%d)+%d", preDeref, postDeref); + break; + case UNWIND_OPCODE_BASE_SP | UNWIND_OPCODEF_DEREF: + DEBUG_PRINT("unwind: *(sp+%d)+%d", preDeref, postDeref); + break; + } +#endif + + // Adjust based on parameter / preDereference adder. + addr += preDeref; + if ((opcode & UNWIND_OPCODEF_DEREF) == 0) { + // All done: return "BASE + param" + return addr; + } + + // Dereference, and add the postDereference adder. + if (bpf_probe_read(&val, sizeof(val), (void*) addr)) { + DEBUG_PRINT("unwind failed to dereference address 0x%lx", addr); + return 0; + } + // Return: "*(BASE + preDeref) + postDeref" + return val + postDeref; +} + +// Stack unwinding in the absence of frame pointers can be a bit involved, so +// this comment explains what the following code does. +// +// One begins unwinding a frame somewhere in the middle of execution. +// On x86_64, registers RIP (PC), RSP (SP), and RBP (FP) are available. +// +// This function resolves a "stack delta" command from from our internal maps. +// This stack delta refers to a rule on how to unwind the state. In the simple +// case it just provides SP delta and potentially offset from where to recover +// FP value. See unwind_register_address() on the expressions supported. +// +// The function sets the bool pointed to by the given `stop` pointer to `false` +// if the main ebpf unwinder should exit. This is the case if the current PC +// is marked with UNWIND_COMMAND_STOP which marks entry points (main function, +// thread spawn function, signal handlers, ...). +#if defined(__x86_64__) +static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, UnwindState *state, bool* stop) { + *stop = false; + + u32 unwindInfo = 0; + u64 rt_regs[18]; + int addrDiff = 0; + u64 cfa = 0; + + // The relevant executable is compiled with frame pointer omission, so + // stack deltas need to be retrieved from the relevant map. + ErrorCode error = get_stack_delta(state->text_section_id, state->text_section_offset, + &addrDiff, &unwindInfo); + if (error) { + return error; + } + + if (unwindInfo & STACK_DELTA_COMMAND_FLAG) { + switch (unwindInfo & ~STACK_DELTA_COMMAND_FLAG) { + case UNWIND_COMMAND_PLT: + // The toolchains routinely emit a fixed DWARF expression to unwind the full + // PLT table with one expression to reduce .eh_frame size. + // This is the hard coded implementation of this expression. For further details, + // see https://hal.inria.fr/hal-02297690/document, page 4. (DOI: 10.1145/3360572) + cfa = state->sp + 8 + ((((state->pc & 15) >= 11) ? 1 : 0) << 3); + DEBUG_PRINT("PLT, cfa=0x%lx", (unsigned long)cfa); + break; + case UNWIND_COMMAND_SIGNAL: + // The rt_sigframe is defined at: + // 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))) { + goto err_native_pc_read; + } + state->r13 = rt_regs[5]; + state->fp = rt_regs[10]; + state->sp = rt_regs[15]; + state->pc = rt_regs[16]; + goto frame_ok; + case UNWIND_COMMAND_STOP: + *stop = true; + return ERR_OK; + default: + return ERR_UNREACHABLE; + } + } else { + UnwindInfo *info = bpf_map_lookup_elem(&unwind_info_array, &unwindInfo); + if (!info) { + increment_metric(metricID_UnwindNativeErrBadUnwindInfoIndex); + return ERR_NATIVE_BAD_UNWIND_INFO_INDEX; + } + + s32 param = info->param; + if (info->mergeOpcode) { + DEBUG_PRINT("AddrDiff %d, merged delta %#02x", addrDiff, info->mergeOpcode); + if (addrDiff >= (info->mergeOpcode & ~MERGEOPCODE_NEGATIVE)) { + param += (info->mergeOpcode & MERGEOPCODE_NEGATIVE) ? -8 : 8; + DEBUG_PRINT("Merged delta match: cfaDelta=%d", unwindInfo); + } + } + + // Resolve the frame's CFA (previous PC is fixed to CFA) address, and + // the previous FP address if any. + cfa = unwind_register_address(state, 0, info->opcode, param); + u64 fpa = unwind_register_address(state, cfa, info->fpOpcode, info->fpParam); + + if (fpa) { + bpf_probe_read(&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))) { + err_native_pc_read: + increment_metric(metricID_UnwindNativeErrPCRead); + return ERR_NATIVE_PC_READ; + } + state->sp = cfa; +frame_ok: + increment_metric(metricID_UnwindNativeFrames); + return ERR_OK; +} +#elif defined(__aarch64__) +static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *state, bool* stop) { + *stop = false; + + u32 unwindInfo = 0; + int addrDiff = 0; + u64 rt_regs[34]; + u64 cfa = 0; + + // The relevant executable is compiled with frame pointer omission, so + // stack deltas need to be retrieved from the relevant map. + ErrorCode error = get_stack_delta(state->text_section_id, state->text_section_offset, + &addrDiff, &unwindInfo); + if (error) { + return error; + } + + if (unwindInfo & STACK_DELTA_COMMAND_FLAG) { + switch (unwindInfo & ~STACK_DELTA_COMMAND_FLAG) { + case UNWIND_COMMAND_SIGNAL: + // On aarch64 the struct rt_sigframe is at: + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/kernel/signal.c?h=v6.4#n39 + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/include/uapi/asm/sigcontext.h?h=v6.4#n28 + // offsetof(struct rt_sigframe, uc.uc_mcontext.regs[0]) = 312 + // 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))) { + goto err_native_pc_read; + } + state->pc = normalize_pac_ptr(rt_regs[32]); + state->sp = rt_regs[31]; + state->fp = rt_regs[29]; + state->lr = normalize_pac_ptr(rt_regs[30]); + state->r22 = rt_regs[22]; + state->lr_valid = true; + goto frame_ok; + case UNWIND_COMMAND_STOP: + *stop = true; + return ERR_OK; + default: + return ERR_UNREACHABLE; + } + } + + UnwindInfo *info = bpf_map_lookup_elem(&unwind_info_array, &unwindInfo); + if (!info) { + increment_metric(metricID_UnwindNativeErrBadUnwindInfoIndex); + DEBUG_PRINT("Giving up due to invalid unwind info array index"); + return ERR_NATIVE_BAD_UNWIND_INFO_INDEX; + } + + s32 param = info->param; + if (info->mergeOpcode) { + DEBUG_PRINT("AddrDiff %d, merged delta %#02x", addrDiff, info->mergeOpcode); + if (addrDiff >= (info->mergeOpcode & ~MERGEOPCODE_NEGATIVE)) { + param += (info->mergeOpcode & MERGEOPCODE_NEGATIVE) ? -8 : 8; + DEBUG_PRINT("Merged delta match: cfaDelta=%d", unwindInfo); + } + } + + // Resolve the frame CFA (previous PC is fixed to CFA) address + cfa = unwind_register_address(state, 0, info->opcode, param); + + // Resolve Return Address, it is either the value of link register or + // stack address where RA is stored + u64 ra = unwind_register_address(state, cfa, info->fpOpcode, info->fpParam); + if (ra) { + 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) { + increment_metric(metricID_UnwindNativeErrLrUnwindingMidTrace); + return ERR_NATIVE_LR_UNWINDING_MID_TRACE; + } + + // set return address location to link register + state->pc = ra; + } else { + DEBUG_PRINT("RA: %016llX", (u64)ra); + + // read the value of RA from stack + if (bpf_probe_read(&state->pc, sizeof(state->pc), (void*)ra)) { + // error reading memory, mark RA as invalid + ra = 0; + } + } + + state->pc = normalize_pac_ptr(state->pc); + } + + if (!ra) { + err_native_pc_read: + // report failure to resolve RA and stop unwinding + increment_metric(metricID_UnwindNativeErrPCRead); + DEBUG_PRINT("Giving up due to failure to resolve RA"); + return ERR_NATIVE_PC_READ; + } + + // Try to resolve frame pointer + // simple heuristic for FP based frames + // the GCC compiler usually generates stack frame records in such a way, + // so that FP/RA pair is at the bottom of a stack frame (stack frame + // record at lower addresses is followed by stack vars at higher ones) + // this implies that if no other changes are applied to the stack such + // as alloca(), following the prolog SP/FP points to the frame record + // itself, in such a case FP offset will be equal to 8 + if (info->fpParam == 8) { + // 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)); + } + } + + state->sp = cfa; + state->lr_valid = false; +frame_ok: + increment_metric(metricID_UnwindNativeFrames); + return ERR_OK; +} +#else + #error unsupported architecture +#endif + +// Initialize state from pt_regs +static inline void copy_state_regs(UnwindState *state, struct pt_regs *regs) +{ +#if defined(__x86_64__) + state->pc = regs->ip; + state->sp = regs->sp; + state->fp = regs->bp; + state->r13 = regs->r13; +#elif defined(__aarch64__) + 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; + } + + // 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; + } + + // 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); + +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"); + } + + return arm64_ptregs_size; +} +#endif + +// Convert kernel stack pointer to pt_regs pointers at the top-of-stack. +static inline void *get_kernel_stack_ptregs(u64 addr) +{ +#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)); +#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); +#endif +} + +// Extract the usermode pt_regs for given context. +// +// 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). +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. + struct pt_regs regs; + if (bpf_probe_read(®s, sizeof(regs), get_kernel_stack_ptregs(ctx->sp))) { + 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; + 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); + } else { + // User mode code interrupted, registers are available via the ebpf context. + copy_state_regs(state, ctx); + } + + DEBUG_PRINT("Read regs: pc: %llx sp: %llx fp: %llx", state->pc, state->sp, state->fp); + *has_usermode_regs = true; + return ERR_OK; +} + +SEC("perf_event/unwind_native") +int unwind_native(struct pt_regs *ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + Trace *trace = &record->trace; + int unwinder; + ErrorCode error; +#pragma unroll + for (int i = 0; i < NATIVE_FRAMES_PER_PROGRAM; i++) { + unwinder = PROG_UNWIND_STOP; + + // Unwind native code + u32 frame_idx = trace->stack_len; + DEBUG_PRINT("==== unwind_native %d ====", frame_idx); + increment_metric(metricID_UnwindNativeAttempts); + + // Push frame first. The PC is valid because a text section mapping was found. + 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); + if (error) { + DEBUG_PRINT("failed to push native frame"); + break; + } + + // Unwind the native frame using stack deltas. Stop if no next frame. + bool stop; + error = unwind_one_frame(trace->pid, frame_idx, &record->state, &stop); + if (error || stop) { + break; + } + + // Continue unwinding + DEBUG_PRINT(" pc: %llx sp: %llx fp: %llx", record->state.pc, record->state.sp, record->state.fp); + error = get_next_unwinder_after_native_frame(record, &unwinder); + if (error || unwinder != PROG_UNWIND_NATIVE) { + break; + } + } + + // Tail call needed for recursion, switching to interpreter unwinder, or reporting + // trace due to end-of-trace or error. The unwinder program index is set accordingly. + record->state.unwind_error = error; + tail_call(ctx, unwinder); + DEBUG_PRINT("bpf_tail call failed for %d in unwind_native", unwinder); + return -1; +} + +static inline +int collect_trace(struct pt_regs *ctx) { + // Get the PID and TGID register. + u64 id = bpf_get_current_pid_tgid(); + u64 pid = id >> 32; + + if (pid == 0) { + return 0; + } + + DEBUG_PRINT("==== do_perf_event ===="); + + // The trace is reused on each call to this function so we have to reset the + // variables used to maintain state. + DEBUG_PRINT("Resetting CPU record"); + PerCPURecord *record = get_pristine_per_cpu_record(); + if (!record) { + return -1; + } + + Trace *trace = &record->trace; + trace->pid = pid; + trace->ktime = bpf_ktime_get_ns(); + if (bpf_get_current_comm(&(trace->comm), sizeof(trace->comm)) < 0) { + increment_metric(metricID_ErrBPFCurrentComm); + } + + // Get the kernel mode stack trace first + trace->kernel_stack_id = bpf_get_stackid(ctx, &kernel_stackmap, BPF_F_REUSE_STACKID); + DEBUG_PRINT("kernel stack id = %d", trace->kernel_stack_id); + + // Recursive unwind frames + int unwinder = PROG_UNWIND_STOP; + bool has_usermode_regs; + 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)) { + increment_metric(metricID_NumProcNew); + } + return 0; + } + error = get_next_unwinder_after_native_frame(record, &unwinder); + +exit: + record->state.unwind_error = error; + tail_call(ctx, unwinder); + DEBUG_PRINT("bpf_tail call failed for %d in native_tracer_entry", unwinder); + return -1; +} + +SEC("perf_event/native_tracer_entry") +int native_tracer_entry(struct bpf_perf_event_data *ctx) { + return collect_trace((struct pt_regs*) &ctx->regs); +} diff --git a/support/ebpf/perl_tracer.ebpf.c b/support/ebpf/perl_tracer.ebpf.c new file mode 100644 index 00000000..0ec4194e --- /dev/null +++ b/support/ebpf/perl_tracer.ebpf.c @@ -0,0 +1,434 @@ +// This file contains the code and map definitions for the Perl tracer + +// Read the interpreterperl.go for generic discussion of the Perl VM. +// +// The trace is extracted from the Perl Context Stack (see perlguts for explanation). +// Basically this stack contains a node for each sub/function/method/regexp/block +// that requires tracking VM context state. The unwinder simply walks these structures +// from top to bottom. During the walk we do two things: +// 1) parse all 'sub' nodes, which represent a function entry point. This node has +// available the activated function's name derived from the runtime object stash. +// The unwinder will resolve this CV object to the canonical EGV (Effective GV) it +// refers to. +// Note: there is no information about where this 'sub' was defined in, or where +// the execution inside it, is currently. The file/line is then taken from the first +// COP seen earlier. See next step. +// 2) parse all nodes of 'block' type (includes also the 'sub' nodes) and records +// deepest available "oldcop" field which basically is the pointer to the "COP" +// (Control OPS) structure. COP is basically the source file AST parse node for +// an expression. It contains the filename and line number of this expression, +// and its where we extract the current source file/line from. +// +// So, when walking the Context Stack, we first expect to see a 'COP' and store it. +// Additional less deep COPs might be seen and ignored. A 'sub' entry indicates +// a function boundary, which is then recorded as the stack frame along with +// the outmost COP seen earlier for the file/line information. +// +// The unwinder will also synchronize so that the context walking is stopped and +// native unwinding is continued based on what the Perl Context Stack indicates. +// This allows synchronizing the Perl functions to the right position in the trace. + +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" +#include +#include + +#include "tracemgmt.h" +#include "types.h" +#include "tsd.h" + +// The number of Perl frames to unwind per frame-unwinding eBPF program. +#define PERL_FRAMES_PER_PROGRAM 12 + +// PERL SI types definitions +// https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L1017-L1035 +#define PERLSI_MAIN 1 + +// PERL_CONTEXT type definitions +// https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L886-L909 +#define CXTYPEMASK 0xf +#define CXt_SUB 9 +#define CXt_FORMAT 10 +#define CXt_EVAL 11 +#define CXt_SUBST 12 + +// Flags for CXt_SUB (and FORMAT) +// https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L912-L917 +#define CXp_SUB_RE_FAKE 0x80 + +// Scalar Value types (SVt) +// https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L132-L166 +#define SVt_MASK 0x1f +#define SVt_PVGV 9 +#define SVt_PVCV 13 + +// https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L375-L377 +#define SVpgv_GP 0x00008000 + +// Code Value flags (CVf) +// https://github.com/Perl/perl5/blob/v5.32.0/cv.h#L115-L140 +#define CVf_NAMED 0x8000 + +// Map from Perl process IDs to a structure containing addresses of variables +// we require in order to build the stack trace +bpf_map_def SEC("maps") perl_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(PerlProcInfo), + .max_entries = 1024, +}; + +// Record a Perl frame +static inline __attribute__((__always_inline__)) +ErrorCode push_perl(Trace *trace, u64 file, u64 line) { + DEBUG_PRINT("Pushing perl frame cop=0x%lx, cv=0x%lx", (unsigned long)file, (unsigned long)line); + return _push(trace, file, line, FRAME_MARKER_PERL); +} + +// resolve_cv_egv() takes in a CV* and follows the pointers to resolve this CV's +// EGV to be reported for HA. This basically maps the internal code value, to its +// canonical symbol name. This mapping is done in EBPF because it seems the CV* +// can get undefined once it goes out of scope, but the EGV should be more permanent. +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)) { + goto err; + } + + if ((cv_flags & SVt_MASK) != SVt_PVCV) { + DEBUG_PRINT("CV is not a PVCV, flags 0x%x", cv_flags); + return 0; + } + + // Follow the any pointer for the XPVCV body + void *xpvcv; + if (bpf_probe_read(&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)) { + goto err; + } + + if ((xcv_flags & CVf_NAMED) == CVf_NAMED) { + // NAMED CVs are created when a function gets undefined, but someone is + // still holding reference to them. Perl VM should ensure that these are + // not seen in the Context Stack. + DEBUG_PRINT("Unexpected NAMED CV, flags 0x%x/0x%x", cv_flags, xcv_flags); + return 0; + } + + // 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) || + !gv) { + goto err; + } + + DEBUG_PRINT("Found GV at 0x%lx", (unsigned long)gv); + + // Make sure we read GV with a GP + u32 gv_flags; + if (bpf_probe_read(&gv_flags, sizeof(gv_flags), gv + perlinfo->sv_flags)) { + goto err; + } + + if ((gv_flags & (SVt_MASK|SVpgv_GP)) != (SVt_PVGV|SVpgv_GP)) { + // Perl VM should also ensure that we see only GV-with-GP type variables + // via the Context stack. + DEBUG_PRINT("Unexpected GV-without-GP, flags 0x%x", gv_flags); + return 0; + } + + // Follow GP pointer + void *gp; + if (bpf_probe_read(&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)) { + goto err; + } + + if (egv) { + DEBUG_PRINT("Found EGV at 0x%lx", (unsigned long)egv); + return egv; + } + return gv; + +err: + DEBUG_PRINT("Bad bpf_probe_read() in resolve_cv_egv"); + increment_metric(metricID_UnwindPerlResolveEGV); + return 0; +} + +static inline __attribute__((__always_inline__)) +int process_perl_frame(PerCPURecord *record, const PerlProcInfo *perlinfo, const void *cx) { + Trace *trace = &record->trace; + int unwinder = PROG_UNWIND_PERL; + + // Per S_dopoptosub_at() we are interested only in specific SUB/FORMAT + // 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)) { + goto err; + } + + DEBUG_PRINT("Got perl cx 0x%x", type); + switch (type & CXTYPEMASK) { + case CXt_SUBST: + // SUBST is special case, it is the only type using different union portion + // of 'struct context' and does not have COP pointer in it. + // Skip these completely. + return unwinder; + case CXt_SUB: + case CXt_FORMAT: + // FORMAT and SUB blocks are quite identical, and the ones we want to show + // in the backtrace. + + // In sub foo { /(?{...})/ }, foo ends up on the CX stack twice; the first for + // the normal foo() call, and the second for a faked up re-entry into the sub + // to execute the code block. Hide this faked entry from the world like perl does. + // https://github.com/Perl/perl5/blob/v5.32.0/pp_ctl.c#L1432-L1462 + if (type & CXp_SUB_RE_FAKE) { + return unwinder; + } + + if (get_next_unwinder_after_interpreter(record) != PROG_UNWIND_STOP) { + // If generating mixed traces, use 'sub_retop' to detect if this is the + // C->Perl boundary. This is the value returned as next opcode at + // https://github.com/Perl/perl5/blob/v5.32.0/pp_hot.c#L4952-L4955 + // and then used by the mainloop to determine if it's time to exit and + // 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)) { + goto err; + } + if (retop == 0) { + unwinder = get_next_unwinder_after_interpreter(record); + } + } + + // Extract the functions Code Value for symbolization + void *cv; + if (bpf_probe_read(&cv, sizeof(cv), cx + perlinfo->context_blk_sub_cv)) { + goto err; + } + + void *egv = resolve_cv_egv(perlinfo, cv); + if (!egv) { + goto err; + } + if (push_perl(trace, (u64)egv, (u64)record->perlUnwindState.cop) != ERR_OK) { + return PROG_UNWIND_STOP; + } + record->perlUnwindState.cop = 0; + break; + default: + // Some other block context type. + break; + } + + // 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)) { + goto err; + } + DEBUG_PRINT("COP from context stack 0x%lx", (unsigned long)record->perlUnwindState.cop); + } + return unwinder; + +err: + // Perl context stack topmost entry might be bogus: the item count is updated + // first and the content is filled later. Thus there is small window to read + // garbage values on the topmost entry. We likely get here for those entries. + // Since this is known race, just continue reading the context stack if nothing + // happened, and rest of the reads should be just fine. + DEBUG_PRINT("Failed to read context stack entry at %p", cx); + increment_metric(metricID_UnwindPerlReadContextStackEntry); + return PROG_UNWIND_PERL; +} + +static inline __attribute__((__always_inline__)) +void prepare_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) { + const void *si = record->perlUnwindState.stackinfo; + // cxstack contains the base of the current context stack which is an array of PERL_CONTEXT + // structures, while cxstack_ix is the index of the current frame within that stack. + 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)) { + DEBUG_PRINT("Failed to read stackinfo at 0x%lx", (unsigned long)si); + unwinder_mark_done(record, PROG_UNWIND_PERL); + increment_metric(metricID_UnwindPerlReadStackInfo); + return; + } + + DEBUG_PRINT("New stackinfo, cxbase 0x%lx, cxix %d", (unsigned long)cxstack, cxix); + record->perlUnwindState.cxbase = cxstack; + record->perlUnwindState.cxcur = cxstack + cxix * perlinfo->context_sizeof; +} + +static inline __attribute__((__always_inline__)) +int walk_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) { + const void *si = record->perlUnwindState.stackinfo; + + // If Perl stackinfo is not available, all frames have been processed, then + // continue with native unwinding. + if (!si) { + return get_next_unwinder_after_interpreter(record); + } + + int unwinder = PROG_UNWIND_PERL; + const void *cxbase = record->perlUnwindState.cxbase; +#pragma unroll + for (u32 i = 0; i < PERL_FRAMES_PER_PROGRAM; ++i) { + // Test first the stack 'cxcur' validity. Some stacks can have 'cxix=-1' + // when they are being constructed or ran. + if (record->perlUnwindState.cxcur < cxbase) { + // End of a stackinfo. Resume to native unwinder if it's active. + break; + } + // Parse one context stack entry. + unwinder = process_perl_frame(record, perlinfo, record->perlUnwindState.cxcur); + record->perlUnwindState.cxcur -= perlinfo->context_sizeof; + if (unwinder == PROG_UNWIND_STOP) { + // Failed to read context stack entry. + break; + } + increment_metric(metricID_UnwindPerlFrames); + if (unwinder != PROG_UNWIND_PERL) { + // Perl context frame which returns to next native frame. + break; + } + } + + if (record->perlUnwindState.cxcur < cxbase) { + // Current Perl context stack exhausted. Check if there's more to unwind. + Trace *trace = &record->trace; + + // If we have still a valid COP cached, it should be reported as the root frame. + // In this case we don't have valid function context, and this implies an anonymous + // or global level code block (e.g. code in file not inside function). + u64 cop = (u64)record->perlUnwindState.cop; + if (cop) { + DEBUG_PRINT("End of perl stack - pushing main 0x%lx", (unsigned long)cop); + if (push_perl(trace, 0, cop) != ERR_OK) { + return PROG_UNWIND_STOP; + } + record->perlUnwindState.cop = 0; + } + + // If the current stackinfo is of type PERLSI_MAIN, we should stop unwinding + // 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) || + type == PERLSI_MAIN || + bpf_probe_read(&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"); + unwinder_mark_done(record, PROG_UNWIND_PERL); + } else { + DEBUG_PRINT("Perl next stackinfo: type %d", type); + record->perlUnwindState.stackinfo = si; + prepare_perl_stack(record, perlinfo); + } + unwinder = get_next_unwinder_after_interpreter(record); + } + + // Stack completed. Prepare the next one. + DEBUG_PRINT("Perl unwind done, next stackinfo 0x%lx, 0x%lx 0x%lx", + (unsigned long)si, (unsigned long)record->perlUnwindState.cxbase, + (unsigned long)record->perlUnwindState.cxcur); + return unwinder; +} + +// unwind_perl 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 +// Perl stack frames to the trace object for the current CPU. +SEC("perf_event/unwind_perl") +int unwind_perl(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_perl()"); + + PerlProcInfo *perlinfo = bpf_map_lookup_elem(&perl_procs, &pid); + if (!perlinfo) { + DEBUG_PRINT("Can't build Perl stack, no address info"); + return 0; + } + + int unwinder = get_next_unwinder_after_interpreter(record); + DEBUG_PRINT("Building Perl stack for 0x%x", perlinfo->version); + + if (!record->perlUnwindState.stackinfo) { + // First Perl main loop encountered. Extract first the Interpreter state. + increment_metric(metricID_UnwindPerlAttempts); + + void *interpreter; + if (perlinfo->stateInTSD) { + void *tsd_base; + if (tsd_get_base(ctx, &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)) { + DEBUG_PRINT("Failed to read tsdKey from 0x%lx", (unsigned long)perlinfo->stateAddr); + goto err_tsd; + } + + if (tsd_read(&perlinfo->tsdInfo, tsd_base, tsd_key, &interpreter)) { + err_tsd: + increment_metric(metricID_UnwindPerlTSD); + goto exit; + } + + DEBUG_PRINT("TSD Base 0x%lx, TSD Key %d", (unsigned long) tsd_base, tsd_key); + } else { + interpreter = (void*)perlinfo->stateAddr; + } + 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)) { + DEBUG_PRINT("Failed to read interpreter state"); + increment_metric(metricID_UnwindPerlReadStackInfo); + goto exit; + } + DEBUG_PRINT("COP from interpreter state 0x%lx", (unsigned long)record->perlUnwindState.cop); + + prepare_perl_stack(record, perlinfo); + } + + // Unwind one call stack or unrolled length, and continue + unwinder = walk_perl_stack(record, perlinfo); + +exit: + tail_call(ctx, unwinder); + return -1; +} diff --git a/support/ebpf/php_tracer.ebpf.c b/support/ebpf/php_tracer.ebpf.c new file mode 100644 index 00000000..1aa6d5a0 --- /dev/null +++ b/support/ebpf/php_tracer.ebpf.c @@ -0,0 +1,265 @@ +// This file contains the code and map definitions for the PHP tracer + +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" +#include "tracemgmt.h" +#include "types.h" + +// The number of PHP frames to unwind per frame-unwinding eBPF program. If +// we start running out of instructions in the walk_php_stack program, one +// option is to adjust this number downwards. +#define FRAMES_PER_WALK_PHP_STACK 19 + +// The type_info flag for executor data to indicate top-of-stack frames +// as defined in php/Zend/zend_compile.h. +#define ZEND_CALL_TOP (1 << 17) + +// zend_function.type values we need from php/Zend/zend_compile.h +#define ZEND_USER_FUNCTION 2 +#define ZEND_EVAL_CODE 4 + +// Map from PHP process IDs to the address of the `executor_globals` for that process +bpf_map_def SEC("maps") php_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(PHPProcInfo), + .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) { + int frame_type = is_jitted ? FRAME_MARKER_PHP_JIT : FRAME_MARKER_PHP; + return _push(trace, file, line, frame_type); +} + +// Record a PHP call for which no function object is available +static inline __attribute__((__always_inline__)) +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, + 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)) { + DEBUG_PRINT("Failed to read current_execute_data->func (0x%lx)", + (unsigned long) (execute_data + phpinfo->zend_execute_data_function)); + return metricID_UnwindPHPErrBadZendExecuteData; + } + + // It is possible there is no function object. + if (!zend_function) { + if (push_unknown_php(trace) != ERR_OK) { + DEBUG_PRINT("failed to push unknown php frame"); + return -1; + } + return metricID_UnwindPHPFrames; + } + + // Get zend_function->type + u8 func_type; + if (bpf_probe_read(&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; + } + + u32 lineno = 0; + 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)) { + 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)) { + DEBUG_PRINT("Failed to read executor_globals->opline->lineno (0x%lx)", + (unsigned long) (zend_op + phpinfo->zend_op_lineno)); + return metricID_UnwindPHPErrBadZendOpline; + } + + // 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)) { + DEBUG_PRINT("Failed to read execute_data->This.type_info (0x%lx)", + (unsigned long) execute_data); + return metricID_UnwindPHPErrBadZendExecuteData; + } + } + + // To give more information to the HA we also pass up the type info. This is safe + // because lineno is 32-bits too. + 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) { + DEBUG_PRINT("failed to push php frame"); + return -1; + } + + return metricID_UnwindPHPFrames; +} + +static inline __attribute__((__always_inline__)) +int walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo* jitinfo) { + const void *execute_data = record->phpUnwindState.zend_execute_data; + bool mixed_traces = get_next_unwinder_after_interpreter(record) != PROG_UNWIND_STOP; + + // If PHP data is not available, all frames have been processed, then + // continue with native unwinding. + if (!execute_data) { + return get_next_unwinder_after_interpreter(record); + } + + int unwinder = PROG_UNWIND_PHP; + 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); + if (metric >= 0) { + increment_metric(metric); + } + if (metric != metricID_UnwindPHPFrames) { + goto err; + } + + // Get current_execute_data->prev_execute_data + if (bpf_probe_read(&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); + goto err; + } + + // Check end-of-stack and end of current interpreter loop stack conditions + if (!execute_data || (mixed_traces && (type_info & ZEND_CALL_TOP))) { + DEBUG_PRINT("Top-of-stack, with next execute_data=0x%lx", (unsigned long) execute_data); + // JIT'd PHP code needs special support for recovering the return address on both amd64 + // and arm. + // Essentially we have two cases here: + // 1) The PC corresponds to something in the interpreter loop. We have stack + // deltas for this, so we don't need to do anything. + // 2) The PC corresponds to something in the JIT region. We don't have stack + // deltas for this, so we need to use the previously recovered address. + // This previously recovered return address corresponds to an address inside + // "execute_ex" (the PHP interpreter loop). In particular, the asm looks like this: + // jmp [r15] + // mov rax, imm <==== This is the return address we previously recovered + // This approach only works because the address we're using here is inside the + // interpreter loop and on the same native stack frame: otherwise we'd need to + // 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)) { + record->state.pc = phpinfo->jit_return_address; + if (resolve_unwind_mapping(record, &unwinder) != ERR_OK) { + unwinder = PROG_UNWIND_STOP; + } + } else { + unwinder = get_next_unwinder_after_interpreter(record); + } + break; + } + } + + + if (!execute_data) { + err: + unwinder_mark_done(record, PROG_UNWIND_PHP); + } + record->phpUnwindState.zend_execute_data = execute_data; + return unwinder; +} + +SEC("perf_event/unwind_php") +int unwind_php(struct pt_regs *ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + int unwinder = get_next_unwinder_after_interpreter(record); + u32 pid = record->trace.pid; + PHPProcInfo *phpinfo = bpf_map_lookup_elem(&php_procs, &pid); + if (!phpinfo) { + DEBUG_PRINT("No PHP introspection data"); + goto exit; + } + + 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)) { + DEBUG_PRINT("Failed to read executor_globals.current_execute data (0x%lx)", + (unsigned long) phpinfo->current_execute_data); + increment_metric(metricID_UnwindPHPErrBadCurrentExecuteData); + goto exit; + } + } + + // 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. + // This is necessary because: + // a) The PHP VM jumps into code by default. This is equivalent to having an inner frame. + // b) The PHP VM allocates some space for alignment purposes and saving registers. + // c) The amount and alignment of this space can change in hard-to-detect ways. + // 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)) { + record->state.sp = record->state.fp; + } +#endif + + 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); + +exit: + tail_call(ctx, unwinder); + return -1; +} diff --git a/support/ebpf/print_instruction_count.sh b/support/ebpf/print_instruction_count.sh new file mode 100755 index 00000000..a7c9e402 --- /dev/null +++ b/support/ebpf/print_instruction_count.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +export SHELLOPTS + +file="$1" + +OBJDUMP_CMD=llvm-objdump +if ! type -p "${OBJDUMP_CMD}"; then + OBJDUMP_CMD=llvm-objdump-13 +fi + +echo -e "\nInstruction counts for ${file}:\n" + +total=0 +while read line; do + name=$(echo $line | awk '{ print $2 }') + size="0x$(echo $line | awk '{ print $3 }')" + size=$((size / 8)) # ebpf has 64-bit fixed length instructions + echo "$name has $size instructions" + total=$((total + size)) +done < <($OBJDUMP_CMD --section-headers "${file}" | grep TEXT) + +echo -e "\nTotal instructions: ${total}\n" diff --git a/support/ebpf/python_tracer.ebpf.c b/support/ebpf/python_tracer.ebpf.c new file mode 100644 index 00000000..fe49ca29 --- /dev/null +++ b/support/ebpf/python_tracer.ebpf.c @@ -0,0 +1,313 @@ +// This file contains the code and map definitions for the Python tracer + +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" +#include +#include + +#include "tracemgmt.h" +#include "types.h" +#include "tsd.h" +#include "errors.h" + +// The number of Python frames to unwind per frame-unwinding eBPF program. If +// we start running out of instructions in the walk_python_stack program, one +// option is to adjust this number downwards. +#define FRAMES_PER_WALK_PYTHON_STACK 12 + +// Forward declaration to avoid warnings like +// "declaration of 'struct pt_regs' will not be visible outside of this function [-Wvisibility]". +struct pt_regs; + +// Map from Python process IDs to a structure containing addresses of variables +// we require in order to build the stack trace +bpf_map_def SEC("maps") py_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(PyProcInfo), + .max_entries = 1024, +}; + +// Record a Python frame +static inline __attribute__((__always_inline__)) +ErrorCode push_python(Trace *trace, u64 file, u64 line) { + return _push(trace, file, line, FRAME_MARKER_PYTHON); +} + +static inline __attribute__((__always_inline__)) +u64 py_encode_lineno(u32 object_id, u32 f_lasti) { + return (object_id | (((u64)f_lasti) << 32)); +} + +static inline __attribute__((__always_inline__)) +ErrorCode process_python_frame(PerCPURecord *record, const PyProcInfo *pyinfo, + void **py_frameobjectptr, bool *continue_with_next) { + Trace *trace = &record->trace; + const void *py_frameobject = *py_frameobjectptr; + u64 lineno = FUNC_TYPE_UNKNOWN, file_id = UNKNOWN_FILE; + u32 codeobject_id; + + *continue_with_next = false; + + // Vars used in extracting data from the Python interpreter + 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)) { + return ERR_UNREACHABLE; + } + + // Read PyFrameObject + if (bpf_probe_read(pss->frame, sizeof(pss->frame), py_frameobject)) { + DEBUG_PRINT( + "Failed to read PyFrameObject 0x%lx", + (unsigned long) py_frameobject); + increment_metric(metricID_UnwindPythonErrBadFrameCodeObjectAddr); + return ERR_PYTHON_BAD_FRAME_OBJECT_ADDR; + } + + void *py_codeobject = *(void**)(&pss->frame[pyinfo->PyFrameObject_f_code]); + *py_frameobjectptr = *(void**)(&pss->frame[pyinfo->PyFrameObject_f_back]); + + // See experiments/python/README.md for a longer version of this. In short, we + // cannot directly obtain the correct Python line number. It has to be calculated + // using information found in the PyCodeObject for the current frame. This + // calculation involves iterating over potentially unbounded data, and so we don't + // want to do it in eBPF. Instead, we log the bytecode instruction that is being + // executed, and then convert this to a line number in the user-land component. + // Bytecode instructions are identified as an offset within a code object. The + // offset is easy to retrieve (PyFrameObject->f_lasti). Code objects are a little + // more tricky. We need to log enough information to uniquely identify the code + // object for the current frame, so that in the user-land component we can load + // it from the .pyc. There is no unique identifier for code objects though, so we + // 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]); + 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])) { + *continue_with_next = true; + } + } + + if (!py_codeobject) { + DEBUG_PRINT( + "Null codeobject for PyFrameObject 0x%lx 0x%lx", + (unsigned long) py_frameobject, + (unsigned long) (py_frameobject + pyinfo->PyFrameObject_f_code)); + increment_metric(metricID_UnwindPythonZeroFrameCodeObject); + goto push_frame; + } + + // Make verifier happy for PyCodeObject offsets + if (pyinfo->PyCodeObject_co_argcount > sizeof(pss->code) - sizeof(int) || + pyinfo->PyCodeObject_co_kwonlyargcount > sizeof(pss->code) - sizeof(int) || + pyinfo->PyCodeObject_co_flags > sizeof(pss->code) - sizeof(int) || + pyinfo->PyCodeObject_co_firstlineno > sizeof(pss->code) - sizeof(int)) { + return ERR_UNREACHABLE; + } + + // Read PyCodeObject + if (bpf_probe_read(pss->code, sizeof(pss->code), py_codeobject)) { + DEBUG_PRINT( + "Failed to read PyCodeObject at 0x%lx", + (unsigned long) (py_codeobject)); + increment_metric(metricID_UnwindPythonErrBadCodeObjectArgCountAddr); + return ERR_PYTHON_BAD_CODE_OBJECT_ADDR; + } + + int py_argcount = *(int*)(&pss->code[pyinfo->PyCodeObject_co_argcount]); + int py_kwonlyargcount = *(int*)(&pss->code[pyinfo->PyCodeObject_co_kwonlyargcount]); + int py_flags = *(int*)(&pss->code[pyinfo->PyCodeObject_co_flags]); + int py_firstlineno = *(int*)(&pss->code[pyinfo->PyCodeObject_co_firstlineno]); + + codeobject_id = (py_argcount << 25) + (py_kwonlyargcount << 18) + + (py_flags << 10) + py_firstlineno; + + file_id = (u64)py_codeobject; + lineno = py_encode_lineno(codeobject_id, (u32)py_f_lasti); + +push_frame: + DEBUG_PRINT("Pushing Python %lx %lu", (unsigned long) file_id, (unsigned long) lineno); + ErrorCode error = push_python(trace, file_id, lineno); + if (error) { + DEBUG_PRINT("failed to push python frame"); + return error; + } + increment_metric(metricID_UnwindPythonFrames); + return ERR_OK; +} + +static inline __attribute__((__always_inline__)) +ErrorCode walk_python_stack(PerCPURecord *record, const PyProcInfo *pyinfo, int *unwinder) { + void *py_frame = record->pythonUnwindState.py_frame; + ErrorCode error = ERR_OK; + *unwinder = PROG_UNWIND_STOP; + +#pragma unroll + for (u32 i = 0; i < FRAMES_PER_WALK_PYTHON_STACK; ++i) { + bool continue_with_next; + error = process_python_frame(record, pyinfo, &py_frame, &continue_with_next); + if (error) { + goto stop; + } + if (continue_with_next) { + *unwinder = get_next_unwinder_after_interpreter(record); + goto stop; + } + if (!py_frame) { + goto stop; + } + } + + *unwinder = PROG_UNWIND_PYTHON; + +stop: + // Set up the state for the next invocation of this unwinding program. + if (error || !py_frame) { + unwinder_mark_done(record, PROG_UNWIND_PYTHON); + } + record->pythonUnwindState.py_frame = py_frame; + return error; +} + +// get_PyThreadState retrieves the PyThreadState* for the current thread. +// +// Python sets the thread_state using pthread_setspecific with the key +// stored in a global variable autoTLSkey. +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)) { + DEBUG_PRINT("Failed to read autoTLSkey from 0x%lx", (unsigned long) autoTLSkeyAddr); + increment_metric(metricID_UnwindPythonErrBadAutoTlsKeyAddr); + return ERR_PYTHON_BAD_AUTO_TLS_KEY_ADDR; + } + + if (tsd_read(&pyinfo->tsdInfo, tsd_base, key, thread_state)) { + increment_metric(metricID_UnwindPythonErrReadThreadStateAddr); + return ERR_PYTHON_READ_THREAD_STATE_ADDR; + } + + return ERR_OK; +} + +static inline __attribute__((__always_inline__)) +ErrorCode get_PyFrame(struct pt_regs *ctx, const PyProcInfo *pyinfo, void **frame) { + void *tsd_base; + if (tsd_get_base(ctx, &tsd_base)) { + DEBUG_PRINT("Failed to get TSD base address"); + increment_metric(metricID_UnwindPythonErrReadTsdBase); + return ERR_PYTHON_READ_TSD_BASE; + } + DEBUG_PRINT("TSD Base 0x%lx, autoTLSKeyAddr 0x%lx", + (unsigned long) tsd_base, + (unsigned long) pyinfo->autoTLSKeyAddr); + + // Get the PyThreadState from TSD + void *py_tsd_thread_state; + ErrorCode error = get_PyThreadState(pyinfo, tsd_base, (void *) pyinfo->autoTLSKeyAddr, + &py_tsd_thread_state); + if (error) { + return error; + } + + if (!py_tsd_thread_state) { + DEBUG_PRINT("PyThreadState is 0x0"); + increment_metric(metricID_UnwindPythonErrZeroThreadState); + return ERR_PYTHON_ZERO_THREAD_STATE; + } + + if (pyinfo->version >= 0x30b) { + // Starting with 3.11 we have to do an additional step to get to _PyInterpreterFrame, formerly + // known as PyFrameObject. + + // Get PyThreadState.cframe + void *cframe_ptr; + if (bpf_probe_read(&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)); + increment_metric(metricID_UnwindPythonErrBadThreadStateFrameAddr); + return ERR_PYTHON_BAD_THREAD_STATE_FRAME_ADDR; + } + + // Get _PyCFrame.current_frame + if (bpf_probe_read(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)); + increment_metric(metricID_UnwindPythonErrBadCFrameFrameAddr); + return ERR_PYTHON_BAD_CFRAME_CURRENT_FRAME_ADDR; + } + } else { + // Get PyThreadState.frame + if (bpf_probe_read(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)); + increment_metric(metricID_UnwindPythonErrBadThreadStateFrameAddr); + return ERR_PYTHON_BAD_THREAD_STATE_FRAME_ADDR; + } + } + + return ERR_OK; +} + +// unwind_python 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 +// Python stack frames to the trace object for the current CPU. +SEC("perf_event/unwind_python") +int unwind_python(struct pt_regs *ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + ErrorCode error = ERR_OK; + int unwinder = get_next_unwinder_after_interpreter(record); + Trace *trace = &record->trace; + u32 pid = trace->pid; + + DEBUG_PRINT("unwind_python()"); + + const PyProcInfo *pyinfo = bpf_map_lookup_elem(&py_procs, &pid); + if (!pyinfo) { + // Not a Python process that we have info on + DEBUG_PRINT("Can't build Python stack, no address info"); + increment_metric(metricID_UnwindPythonErrNoProcInfo); + return ERR_PYTHON_NO_PROC_INFO; + } + + 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); + if (error) { + goto exit; + } + } + if (!record->pythonUnwindState.py_frame) { + DEBUG_PRINT(" -> Python frames are handled"); + unwinder_mark_done(record, PROG_UNWIND_PYTHON); + goto exit; + } + + error = walk_python_stack(record, pyinfo, &unwinder); + +exit: + record->state.unwind_error = error; + tail_call(ctx, unwinder); + return -1; +} diff --git a/support/ebpf/ruby_tracer.ebpf.c b/support/ebpf/ruby_tracer.ebpf.c new file mode 100644 index 00000000..d818f087 --- /dev/null +++ b/support/ebpf/ruby_tracer.ebpf.c @@ -0,0 +1,275 @@ +// This file contains the code and map definitions for the Ruby tracer + +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" +#include "tracemgmt.h" +#include "types.h" + +// Map from Ruby process IDs to a structure containing addresses of variables +// we require in order to build the stack trace +bpf_map_def SEC("maps") ruby_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(RubyProcInfo), + .max_entries = 1024, +}; + +// The number of Ruby frames to unwind per frame-unwinding eBPF program. If +// we start running out of instructions in the walk_ruby_stack program, one +// option is to adjust this number downwards. +#define FRAMES_PER_WALK_RUBY_STACK 27 + +// Ruby VM frame flags are internal indicators for the VM interpreter to +// treat frames in a dedicated way. +// https://github.com/ruby/ruby/blob/5741ae379b2037ad5968b6994309e1d25cda6e1a/vm_core.h#L1208 +#define RUBY_FRAME_FLAG_BMETHOD 0x0040 +#define RUBY_FRAME_FLAG_LAMBDA 0x0100 + +// Record a Ruby frame +static inline __attribute__((__always_inline__)) +ErrorCode push_ruby(Trace *trace, u64 file, u64 line) { + return _push(trace, file, line, FRAME_MARKER_RUBY); +} + +// walk_ruby_stack processes a Ruby VM stack, extracts information from the individual frames and +// pushes this information to user space for symbolization of these frames. +// +// Ruby unwinder workflow: +// From the current execution context struct [0] we can get pointers to the current Ruby VM stack +// as well as to the current call frame pointer (cfp). +// On the Ruby VM stack we have for each cfp one struct [1]. These cfp structs then point to +// instruction sequence (iseq) structs [2] that store the information about file and function name +// that we forward to user space for the symbolization process of the frame. +// +// +// [0] rb_execution_context_struct +// https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L843 +// +// [1] rb_control_frame_struct +// https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L760 +// +// [2] rb_iseq_struct +// https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L456 +static inline __attribute__((__always_inline__)) +ErrorCode walk_ruby_stack(PerCPURecord *record, const RubyProcInfo *rubyinfo, + const void *current_ctx_addr, int* next_unwinder) { + if (!current_ctx_addr) { + *next_unwinder = get_next_unwinder_after_interpreter(record); + return ERR_OK; + } + + Trace *trace = &record->trace; + + *next_unwinder = PROG_UNWIND_STOP; + + // stack_ptr points to the frame of the Ruby VM call stack that will be unwound next + void *stack_ptr = record->rubyUnwindState.stack_ptr; + // last_stack_frame points to the last frame on the Ruby VM stack we want to process + void *last_stack_frame = record->rubyUnwindState.last_stack_frame; + + if (!stack_ptr || !last_stack_frame) { + // stack_ptr_current points to the current frame in the Ruby VM call stack + void *stack_ptr_current; + // stack_size does not reflect the number of frames on the Ruby VM stack + // but contains the current stack size in words. + // stack_size = size in word (size in bytes / sizeof(VALUE)) + // 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))) { + 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))) { + DEBUG_PRINT("ruby: failed to get stack size"); + increment_metric(metricID_UnwindRubyErrReadStackSize); + return ERR_RUBY_READ_STACK_SIZE; + } + + // Calculate the base of the stack so we can calculate the number of frames from it. + // Ruby places two dummy frames on the Ruby VM stack in which we are not interested. + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_backtrace.c#L477-L485 + 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))) { + DEBUG_PRINT("ruby: failed to get cfp"); + increment_metric(metricID_UnwindRubyErrReadCfp); + return ERR_RUBY_READ_CFP; + } + } + + // iseq_addr holds the address to a rb_iseq_struct struct + void *iseq_addr; + // iseq_body points to a rb_iseq_constant_body struct + void *iseq_body; + // pc stores the Ruby VM program counter information + u64 pc; + // iseq_encoded holds the instruction address and operands of a particular instruction sequence + // The format of this element is documented in: + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L328-L348 + u64 iseq_encoded; + // iseq_size holds the size in bytes of a particular instruction sequence + u32 iseq_size; + s64 n; + +#pragma unroll + for (u32 i = 0; i < FRAMES_PER_WALK_RUBY_STACK; ++i) { + 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)); + // 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) { + // Ruby frames without a PC or iseq are special frames and do not hold information + // we can use further on. So we either skip them or ask the native unwinder to continue. + + if (rubyinfo->version < 0x20600) { + // With Ruby version 2.6 the scope of our entry symbol ruby_current_execution_context_ptr + // got extended. We need this extension to jump back unwinding Ruby VM frames if we + // continue at this point with unwinding native frames. + // As this is not available for Ruby versions < 2.6 we just skip this indicator frame and + // continue unwinding Ruby VM frames. Due to this issue, the ordering of Ruby and native + // frames might not be correct for Ruby versions < 2.6. + goto skip; + } + + u64 ep = 0; + if (bpf_probe_read(&ep, sizeof(ep), (void *)(stack_ptr + rubyinfo->ep))) { + DEBUG_PRINT("ruby: failed to get ep"); + increment_metric(metricID_UnwindRubyErrReadEp); + return ERR_RUBY_READ_EP; + } + + if ((ep & (RUBY_FRAME_FLAG_LAMBDA | RUBY_FRAME_FLAG_BMETHOD)) == (RUBY_FRAME_FLAG_LAMBDA | RUBY_FRAME_FLAG_BMETHOD) ) { + // When identifying Ruby lambda blocks at this point, we do not want to return to the + // native unwinder. So we just skip this Ruby VM frame. + goto skip; + } + + stack_ptr += rubyinfo->size_of_control_frame_struct; + *next_unwinder = PROG_UNWIND_NATIVE; + goto save_state; + } + + if (bpf_probe_read(&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))) { + 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))) { + DEBUG_PRINT("ruby: failed to get iseq size"); + increment_metric(metricID_UnwindRubyErrReadIseqSize); + return ERR_RUBY_READ_ISEQ_SIZE; + } + + // To get the line number iseq_encoded is subtracted from pc. This result also represents the size + // of the current instruction sequence. If the calculated size of the instruction sequence is greater + // than the value in iseq_encoded we don't report this pc to user space. + // + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_backtrace.c#L47-L48 + n = (pc - iseq_encoded) / rubyinfo->size_of_value; + if (n > iseq_size || n < 0) { + DEBUG_PRINT("ruby: skipping invalid instruction sequence"); + goto skip; + } + + // For symbolization of the frame we forward the information about the instruction sequence + // and program counter to user space. + // From this we can then extract information like file or function name and line number. + ErrorCode error = push_ruby(trace, (u64)iseq_body, pc); + if (error) { + DEBUG_PRINT("ruby: failed to push frame"); + return error; + } + increment_metric(metricID_UnwindRubyFrames); + + skip: + if (last_stack_frame <= stack_ptr ) { + // We have processed all frames in the Ruby VM and can stop here. + *next_unwinder = PROG_UNWIND_NATIVE; + return ERR_OK; + } + stack_ptr += rubyinfo->size_of_control_frame_struct; + } + *next_unwinder = PROG_UNWIND_RUBY; + +save_state: + // Store the current progress in the Ruby unwind state so we can continue walking the stack + // after the tail call. + record->rubyUnwindState.stack_ptr = stack_ptr; + record->rubyUnwindState.last_stack_frame = last_stack_frame; + + return ERR_OK; +} + +SEC("perf_event/unwind_ruby") +int unwind_ruby(struct pt_regs *ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + int unwinder = get_next_unwinder_after_interpreter(record); + ErrorCode error = ERR_OK; + u32 pid = record->trace.pid; + RubyProcInfo *rubyinfo = bpf_map_lookup_elem(&ruby_procs, &pid); + if (!rubyinfo) { + DEBUG_PRINT("No Ruby introspection data"); + error = ERR_RUBY_NO_PROC_INFO; + increment_metric(metricID_UnwindRubyErrNoProcInfo); + goto exit; + } + + increment_metric(metricID_UnwindRubyAttempts); + + + // Pointer for an address to a rb_execution_context_struct struct. + void *current_ctx_addr = NULL; + + if (rubyinfo->version >= 0x30000) { + // With Ruby 3.x and its internal change of the execution model, we can no longer + // access rb_execution_context_struct directly. Therefore we have to first lookup + // ruby_single_main_ractor and get access to the current execution context via + // 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)) { + goto exit; + } + + if (bpf_probe_read(¤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)) { + goto exit; + } + } + + if (!current_ctx_addr) { + goto exit; + } + + error = walk_ruby_stack(record, rubyinfo, current_ctx_addr, &unwinder); + +exit: + record->state.unwind_error = error; + tail_call(ctx, unwinder); + return -1; +} diff --git a/support/ebpf/sched_monitor.ebpf.c b/support/ebpf/sched_monitor.ebpf.c new file mode 100644 index 00000000..f5c64556 --- /dev/null +++ b/support/ebpf/sched_monitor.ebpf.c @@ -0,0 +1,36 @@ +// This file contains the code and map definitions for the tracepoint on the scheduler to +// report the stopping a process. + +#include "bpfdefs.h" +#include "tracemgmt.h" + +#include "types.h" + +// tracepoint__sched_process_exit is a tracepoint attached to the scheduler that stops processes. +// Every time a processes stops this hook is triggered. +SEC("tracepoint/sched/sched_process_exit") +int tracepoint__sched_process_exit(void *ctx) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = (u32)(pid_tgid >> 32); + u32 tid = (u32)(pid_tgid & 0xFFFFFFFF); + + if (pid != tid) { + // Only if the thread group ID matched with the PID the process itself exits. If they don't + // match only a thread of the process stopped and we do not need to report this PID to + // userspace for further processing. + goto exit; + } + + if (!bpf_map_lookup_elem(&reported_pids, &pid) && !pid_information_exists(ctx, pid)) { + // Only report PIDs that we explicitly track. This avoids sending kernel worker PIDs + // to userspace. + goto exit; + } + + if (report_pid(ctx, pid, false)) { + increment_metric(metricID_NumProcExit); + } + +exit: + return 0; +} diff --git a/support/ebpf/stackdeltatypes.h b/support/ebpf/stackdeltatypes.h new file mode 100644 index 00000000..633fad44 --- /dev/null +++ b/support/ebpf/stackdeltatypes.h @@ -0,0 +1,36 @@ +#ifndef OPTI_STACKDELTATYPES_H +#define OPTI_STACKDELTATYPES_H + +// Command without arguments, the argument is instead an UNWIND_COMMAND_* value +#define UNWIND_OPCODE_COMMAND 0x00 +// Expression with base value being the Canonical Frame Address (CFA) +#define UNWIND_OPCODE_BASE_CFA 0x01 +// Expression with base value being the Stack Pointer +#define UNWIND_OPCODE_BASE_SP 0x02 +// Expression with base value being the Frame Pointer +#define UNWIND_OPCODE_BASE_FP 0x03 +// Expression with base value being the Link Register (ARM64) +#define UNWIND_OPCODE_BASE_LR 0x04 +// An opcode flag to indicate that the value should be dereferenced +#define UNWIND_OPCODEF_DEREF 0x80 + +// Unsupported or no value for the register +#define UNWIND_COMMAND_INVALID 0 +// For CFA: stop unwinding, this function is a stack root function +#define UNWIND_COMMAND_STOP 1 +// Unwind a PLT entry +#define UNWIND_COMMAND_PLT 2 +// Unwind a signal frame +#define UNWIND_COMMAND_SIGNAL 3 + +// If opcode has UNWIND_OPCODEF_DEREF set, the lowest bits of 'param' are used +// as second adder as post-deref operation. This contains the mask for that. +// This assumes that stack and CFA are aligned to register size, so that the +// lowest bits of the offsets are always unset. +#define UNWIND_DEREF_MASK 7 + +// The argument after dereference is multiplied by this to allow some range. +// This assumes register size offsets are used. +#define UNWIND_DEREF_MULTIPLIER 8 + +#endif diff --git a/support/ebpf/system_config.ebpf.c b/support/ebpf/system_config.ebpf.c new file mode 100644 index 00000000..b797dce4 --- /dev/null +++ b/support/ebpf/system_config.ebpf.c @@ -0,0 +1,10 @@ +#include "bpfdefs.h" +#include "types.h" + + +struct bpf_map_def SEC("maps") system_config = { + .type = BPF_MAP_TYPE_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(SystemConfig), + .max_entries = 1, +}; diff --git a/support/ebpf/tracemgmt.h b/support/ebpf/tracemgmt.h new file mode 100644 index 00000000..37a44255 --- /dev/null +++ b/support/ebpf/tracemgmt.h @@ -0,0 +1,385 @@ +// Provides functionality for adding frames to traces, hashing traces and +// updating trace counts + +#ifndef OPTI_TRACEMGMT_H +#define OPTI_TRACEMGMT_H + +#include "bpfdefs.h" +#include "extmaps.h" +#include "frametypes.h" +#include "types.h" +#include "errors.h" + +// increment_metric increments the value of the given metricID by 1 +static inline __attribute__((__always_inline__)) +void increment_metric(u32 metricID) { + u64 *count = bpf_map_lookup_elem(&metrics, &metricID); + if (count) { + ++*count; + } else { + DEBUG_PRINT("Failed to lookup metrics map for metricID %d", metricID); + } +} + +// 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 +// to pass the verifier. If the implementation of get_per_cpu_record() is changed so that NULL can +// be returned, also add an error metric. +static inline PerCPURecord *get_per_cpu_record(void) +{ + int key0 = 0; + return bpf_map_lookup_elem(&per_cpu_records, &key0); +} + +// Return the per-cpu record initialized with pristine values for state variables. +// The return value of get_pristine_per_cpu_record() can never be NULL and return value checks +// only exist to pass the verifier. If the implementation of get_pristine_per_cpu_record() is changed +// so that NULL can be returned, also add an error metric. +static inline PerCPURecord *get_pristine_per_cpu_record() +{ + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return record; + + record->state.pc = 0; + record->state.sp = 0; + record->state.fp = 0; +#if defined(__x86_64__) + record->state.r13 = 0; +#elif defined(__aarch64__) + record->state.lr = 0; + record->state.r22 = 0; + record->state.lr_valid = false; +#endif + record->state.error_metric = -1; + record->state.unwind_error = ERR_OK; + record->perlUnwindState.stackinfo = 0; + record->perlUnwindState.cop = 0; + record->pythonUnwindState.py_frame = 0; + record->phpUnwindState.zend_execute_data = 0; + record->rubyUnwindState.stack_ptr = 0; + record->rubyUnwindState.last_stack_frame = 0; + record->unwindersDone = 0; + record->tailCalls = 0; + + Trace *trace = &record->trace; + trace->kernel_stack_id = -1; + trace->stack_len = 0; + trace->pid = 0; + + // TODO: memset trace to all-zero here? + + return record; +} + +// unwinder_is_done checks if a given unwinder program is done for the trace +// extraction round. +static inline __attribute__((__always_inline__)) +bool unwinder_is_done(const PerCPURecord *record, int unwinder) { + return (record->unwindersDone & (1U << unwinder)) != 0; +} + +// unwinder_mark_done will mask out a given unwinder program so that it will +// not be called again for the same trace. Used when interpreter unwinder has +// extracted all interpreter frames it can extract. +static inline __attribute__((__always_inline__)) +void unwinder_mark_done(PerCPURecord *record, int unwinder) { + record->unwindersDone |= 1U << unwinder; +} + +// Push the file ID, line number and frame type into FrameList with a user-defined +// maximum stack size. +// +// NOTE: The line argument is used for a lot of different purposes, depending on +// the frame type. For example error frames use it to store the error number, +// and hotspot puts a subtype and BCI indices, amongst other things (see +// 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) { + if (trace->stack_len >= max_frames) { + DEBUG_PRINT("unable to push frame: stack is full"); + increment_metric(metricID_UnwindErrStackLengthExceeded); + return ERR_STACK_LENGTH_EXCEEDED; + } + +#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); + trace->stack_len++; + return __push_frame(__cgo_ctx->id, file, line, frame_type); +#else + trace->frames[trace->stack_len++] = (Frame) { + .file_id = file, + .addr_or_line = line, + .kind = frame_type, + }; + + return ERR_OK; +#endif +} + +// 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); +} + +// 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); +} + +// Send a trace to user-land via the `trace_events` perf event buffer. +static inline __attribute__((__always_inline__)) +void send_trace(void *ctx, Trace *trace) { + const u64 num_empty_frames = (MAX_FRAME_UNWINDS - trace->stack_len); + const u64 send_size = sizeof(Trace) - sizeof(Frame) * num_empty_frames; + + if (send_size > sizeof(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; +} + +// resolve_unwind_mapping decodes the current PC's mapping and prepares unwinding information. +// The state text_section_id and text_section_offset are updated accordingly. The unwinding program +// index that should be used is writen to the given `unwinder` pointer. +static ErrorCode resolve_unwind_mapping(PerCPURecord *record, int* unwinder) { + UnwindState *state = &record->state; + pid_t pid = record->trace.pid; + u64 pc = state->pc; + + if (is_kernel_address(pc)) { + // This should not happen as we should only be unwinding usermode stacks. + // Seeing PC point to a kernel address indicates a bad unwind. + DEBUG_PRINT("PC value %lx is a kernel address", (unsigned long) pc); + state->error_metric = metricID_UnwindNativeErrKernelAddress; + return ERR_NATIVE_UNEXPECTED_KERNEL_ADDRESS; + } + + if (pc < 0x1000) { + // The kernel will always return a start address for user space memory mappings that is + // above the value defined in /proc/sys/vm/mmap_min_addr. + // As such small PC values happens regularly (e.g. by handling or extracting the + // PC value incorrectly) we track them but don't proceed with unwinding. + DEBUG_PRINT("small pc value %lx, ignoring", (unsigned long) pc); + state->error_metric = metricID_UnwindNativeSmallPC; + return ERR_NATIVE_SMALL_PC; + } + + PIDPage key = {}; + key.prefixLen = BIT_WIDTH_PID + BIT_WIDTH_PAGE; + key.pid = __constant_cpu_to_be32((u32) pid); + key.page = __constant_cpu_to_be64(pc); + + // Check if we have the data for this virtual address + PIDPageMappingInfo* val = bpf_map_lookup_elem(&pid_page_to_mapping_info, &key); + if (!val) { + DEBUG_PRINT("Failure to look up interval memory mapping for PC 0x%lx", + (unsigned long) pc); + state->error_metric = metricID_UnwindNativeErrWrongTextSection; + return ERR_NATIVE_NO_PID_PAGE_MAPPING; + } + + decode_bias_and_unwind_program(val->bias_and_unwind_program, &state->text_section_bias, unwinder); + state->text_section_id = val->file_id; + state->text_section_offset = pc - state->text_section_bias; + DEBUG_PRINT("Text section id for PC %lx is %llx (unwinder %d)", + (unsigned long) pc, state->text_section_id, *unwinder); + DEBUG_PRINT("Text section bias is %llx, and offset is %llx", + state->text_section_bias, state->text_section_offset); + + return ERR_OK; +} + +// get_next_interpreter tries to get the next interpreter unwinder from the section id. +// If the section id happens to be within the range of a known interpreter it will +// return the interpreter unwinder otherwise the native unwinder. +static inline int get_next_interpreter(PerCPURecord *record) { + UnwindState *state = &record->state; + u64 section_id = state->text_section_id; + u64 section_offset = state->text_section_offset; + // Check if the section id happens to be in the interpreter map. + OffsetRange *range = bpf_map_lookup_elem(&interpreter_offsets, §ion_id); + if (range != 0) { + if ((section_offset >= range->lower_offset) && (section_offset <= range->upper_offset)) { + DEBUG_PRINT("interpreter_offsets match %d", range->program_index); + if (!unwinder_is_done(record, range->program_index)) { + increment_metric(metricID_UnwindCallInterpreter); + return range->program_index; + } + DEBUG_PRINT("interpreter unwinder done"); + } + } + return PROG_UNWIND_NATIVE; +} + +// get_next_unwinder_after_native_frame determines the next unwinder program to run +// after a native stack frame has been unwound. +static inline __attribute__((__always_inline__)) +ErrorCode get_next_unwinder_after_native_frame(PerCPURecord *record, int *unwinder) { + UnwindState *state = &record->state; + *unwinder = PROG_UNWIND_STOP; + + if (state->pc == 0) { + DEBUG_PRINT("Stopping unwind due to unwind failure (PC == 0)"); + state->error_metric = metricID_UnwindErrZeroPC; + return ERR_NATIVE_ZERO_PC; + } + + DEBUG_PRINT("==== Resolve next frame unwinder: frame %d ====", record->trace.stack_len); + ErrorCode error = resolve_unwind_mapping(record, unwinder); + if (error) { + return error; + } + + if (*unwinder == PROG_UNWIND_NATIVE) { + *unwinder = get_next_interpreter(record); + } + + return ERR_OK; +} + +// get_next_unwinder_after_interpreter determines the next unwinder program to run +// after an interpreter (non-native) frame sequence has been unwound. +static inline __attribute__((__always_inline__)) +int get_next_unwinder_after_interpreter(const PerCPURecord *record) { + // Since interpreter-only frame decoding is no longer supported, this + // currently equals to just resuming native unwinding. + return PROG_UNWIND_NATIVE; +} + +// tail_call is a wrapper around bpf_tail_call() and ensures that the number of tail calls is not +// reached while unwinding the stack. +static inline __attribute__((__always_inline__)) +void tail_call(void *ctx, int next) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) { + bpf_tail_call(ctx, &progs, PROG_UNWIND_STOP); + // In theory bpf_tail_call() should never return. But due to instruction reordering by the + // compiler we have to place return here to bribe the verifier to accept this. + return; + } + + if (record->tailCalls >= 29 ) { + // The maximum tail call count we need to support on older kernels is 32. At this point + // there is a chance that continuing unwinding the stack would further increase the number of + // tail calls. As a result we might lose the unwound stack as no further tail calls are left + // to report it to user space. To make sure we do not run into this issue we stop unwinding + // the stack at this point and report it to userspace. + next = PROG_UNWIND_STOP; + record->state.unwind_error = ERR_MAX_TAIL_CALLS; + increment_metric(metricID_MaxTailCalls); + } + record->tailCalls += 1 ; + + bpf_tail_call(ctx, &progs, next); +} + +#endif diff --git a/support/ebpf/tsd.ebpf.c b/support/ebpf/tsd.ebpf.c new file mode 100644 index 00000000..d007f136 --- /dev/null +++ b/support/ebpf/tsd.ebpf.c @@ -0,0 +1,57 @@ +// 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 new file mode 100644 index 00000000..8e1fa4b9 --- /dev/null +++ b/support/ebpf/tsd.h @@ -0,0 +1,63 @@ +#ifndef OPTI_TSD_H +#define OPTI_TSD_H + +#include "bpfdefs.h" + +// tsd_read reads from the Thread Specific Data location associated with the provided key. +static inline __attribute__((__always_inline__)) +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)) { + goto err; + } + } + + 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)) { + goto err; + } + return 0; + +err: + DEBUG_PRINT("Failed to read TSD from 0x%lx", (unsigned long) tsd_addr); + increment_metric(metricID_UnwindErrBadTSDAddr); + return -1; +} + +// 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) { +#ifdef TESTING_COREDUMP + *tsd_base = (void *) __cgo_ctx->tp_base; + return 0; +#else + u32 key = 0; + SystemConfig* syscfg = bpf_map_lookup_elem(&system_config, &key); + if (!syscfg) { + // Unreachable: array maps are always fully initialized. + return -1; + } + + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + + // We need to read task->thread.fsbase (on x86_64), but we can't do so because + // we might have been compiled with different kernel headers, so the struct layout + // is likely to be different. + // 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)) { + DEBUG_PRINT("Failed to read tpbase value"); + increment_metric(metricID_UnwindErrBadTPBaseAddr); + return -1; + } + + return 0; +#endif +} + +#endif // OPTI_TSD_H diff --git a/support/ebpf/types.h b/support/ebpf/types.h new file mode 100644 index 00000000..1f213792 --- /dev/null +++ b/support/ebpf/types.h @@ -0,0 +1,764 @@ +// Provides type definitions shared by the eBPF and Go components + +#ifndef OPTI_TYPES_H +#define OPTI_TYPES_H + +#include +#include +#include "inttypes.h" +#include "errors.h" + +// ID values used as index to maps/metrics array. +// If you add enums below please update the following places too: +// - The host agent ebpf metricID to DB IDMetric translation table in: +// tracer/tracer.go/(StartMapMonitors). +// - The ebpf userland test code metricID stringification table in: +// support/ebpf/tests/tostring.c +enum { + // number of calls to interpreter unwinding in get_next_interpreter() + metricID_UnwindCallInterpreter = 0, + + // number of failures due to PC == 0 in unwind_next_frame() + metricID_UnwindErrZeroPC, + + // number of times MAX_STACK_LEN has been exceeded + metricID_UnwindErrStackLengthExceeded, + + // number of failures to read the TSD address + metricID_UnwindErrBadTSDAddr, + + // number of failures to read the TSD base in get_tls_base() + metricID_UnwindErrBadTPBaseAddr, + + // number of attempted unwinds + metricID_UnwindNativeAttempts, + + // number of unwound frames + metricID_UnwindNativeFrames, + + // number of native unwinds successfully ending with a stop delta + metricID_UnwindNativeStackDeltaStop, + + // number of failures to look up ranges for text section in get_stack_delta() + metricID_UnwindNativeErrLookupTextSection, + + // number of failed range searches within 20 steps in get_stack_delta() + metricID_UnwindNativeErrLookupIterations, + + // number of failures to get StackUnwindInfo from stack delta map in get_stack_delta() + metricID_UnwindNativeErrLookupRange, + + // number of kernel addresses passed to get_text_section() + metricID_UnwindNativeErrKernelAddress, + + // number of failures to find the text section in get_text_section() + metricID_UnwindNativeErrWrongTextSection, + + // number of invalid stack deltas in the native unwinder + metricID_UnwindNativeErrStackDeltaInvalid, + + // number of failures to read PC from stack + metricID_UnwindNativeErrPCRead, + + // number of attempted perl unwinds + metricID_UnwindPerlAttempts, + + // number of perl frames unwound + metricID_UnwindPerlFrames, + + // number of failures to read perl TSD info + metricID_UnwindPerlTSD, + + // number of failures to read perl stack info + metricID_UnwindPerlReadStackInfo, + + // number of failures to read perl context stack entry + metricID_UnwindPerlReadContextStackEntry, + + // number of failures to resolve perl EGV + metricID_UnwindPerlResolveEGV, + + // number of attempted python unwinds + metricID_UnwindPythonAttempts, + + // number of unwound python frames + metricID_UnwindPythonFrames, + + // number of failures to read from pyinfo->pyThreadStateCurrentAddr + metricID_UnwindPythonErrBadPyThreadStateCurrentAddr, + + // number of PyThreadState being 0x0 + metricID_UnwindPythonErrZeroThreadState, + + // number of failures to read PyThreadState.frame in unwind_python() + metricID_UnwindPythonErrBadThreadStateFrameAddr, + + // number of failures to read PyFrameObject->f_back in walk_python_stack() + metricID_UnwindPythonErrBadFrameObjectBackAddr, + + // number of failures to read PyFrameObject->f_code in process_python_frame() + metricID_UnwindPythonErrBadFrameCodeObjectAddr, + + // number of NULL code objects found in process_python_frame() + metricID_UnwindPythonZeroFrameCodeObject, + + // number of failures to get the last instruction address in process_python_frame() + metricID_UnwindPythonErrBadFrameLastInstructionAddr, + + // number of failures to get code object's argcount in process_python_frame() + metricID_UnwindPythonErrBadCodeObjectArgCountAddr, + + // number of failures to get code object's kwonlyargcount in process_python_frame() + metricID_UnwindPythonErrBadCodeObjectKWOnlyArgCountAddr, + + // number of failures to get code object's flags in process_python_frame() + metricID_UnwindPythonErrBadCodeObjectFlagsAddr, + + // number of failures to get code object's first line number in process_python_frame() + metricID_UnwindPythonErrBadCodeObjectFirstLineNumberAddr, + + // number of attempted PHP unwinds + metricID_UnwindPHPAttempts, + + // number of unwound PHP frames + metricID_UnwindPHPFrames, + + // number of failures to read PHP current execute data pointer + metricID_UnwindPHPErrBadCurrentExecuteData, + + // number of failures to read PHP execute data contents + metricID_UnwindPHPErrBadZendExecuteData, + + // number of failures to read PHP zend function contents + metricID_UnwindPHPErrBadZendFunction, + + // number of failures to read PHP zend opline contents + metricID_UnwindPHPErrBadZendOpline, + + // number of times unwind_stop is called without a trace + metricID_ErrEmptyStack, + + // number of attempted Hotspot unwinds + metricID_UnwindHotspotAttempts, + + // number of unwound Hotspot frames + metricID_UnwindHotspotFrames, + + // number of failures to get codeblob address (no heap or bad segmap) + metricID_UnwindHotspotErrNoCodeblob, + + // number of failures to get codeblob data or match it to current unwind state + metricID_UnwindHotspotErrInvalidCodeblob, + + // number of failures to unwind interpreter due to invalid FP + metricID_UnwindHotspotErrInterpreterFP, + + // number of failures to unwind because return address was not found with heuristic + metricID_UnwindHotspotErrInvalidRA, + + // number of times the unwind instructions requested LR unwinding mid-trace + metricID_UnwindHotspotErrLrUnwindingMidTrace, + + // number of times we encountered frame sizes larger than the supported maximum + metricID_UnwindHotspotUnsupportedFrameSize, + + // number of times that PC hold a value smaller than 0x1000 + metricID_UnwindNativeSmallPC, + + // number of times that a lookup of a inner map for stack deltas failed + metricID_UnwindNativeErrLookupStackDeltaInnerMap, + + // number of times that a lookup of the outer map for stack deltas failed + metricID_UnwindNativeErrLookupStackDeltaOuterMap, + + // number of times the bpf helper failed to get the current comm of the task + metricID_ErrBPFCurrentComm, + + // number of attempted Ruby unwinds + metricID_UnwindRubyAttempts, + + // number of unwound Ruby frames + metricID_UnwindRubyFrames, + + // number of attempted V8 unwinds + metricID_UnwindV8Attempts, + + // number of unwound V8 frames + metricID_UnwindV8Frames, + + // number of failures to read V8 frame pointer data + metricID_UnwindV8ErrBadFP, + + // number of failures to read V8 JSFunction object + metricID_UnwindV8ErrBadJSFunc, + + // number of failures to read V8 Code object + metricID_UnwindV8ErrBadCode, + + // number of times frame unwinding failed because of LR == 0 + metricID_UnwindNativeLr0, + + // number of times we failed to update maps/reported_pids + metricID_ReportedPIDsErr, + + // number of times we failed to update maps/pid_events + metricID_PIDEventsErr, + + // number of "process new" PIDs written to maps/pid_events + metricID_NumProcNew, + + // number of "process exit" PIDs written to maps/pid_events + metricID_NumProcExit, + + // number of "unknown PC" PIDs written to maps/pid_events + metricID_NumUnknownPC, + + // number of GENERIC_PID event sent to user space (perf_event) + metricID_NumGenericPID, + + // number of failures to read _PyCFrame.current_frame in unwind_python() + metricID_UnwindPythonErrBadCFrameFrameAddr, + + // number of times stack unwinding was stopped to not exceed the limit of tail calls + metricID_MaxTailCalls, + + // number of times we didn't find an entry for this process in the Python process info array + metricID_UnwindPythonErrNoProcInfo, + + // number of failures to read autoTLSkey + metricID_UnwindPythonErrBadAutoTlsKeyAddr, + + // number of failures to read the thread state pointer from TLD + metricID_UnwindPythonErrReadThreadStateAddr, + + // number of failures to determine the base address for thread-specific data + metricID_UnwindPythonErrReadTsdBase, + + // number of times no entry for a process existed in the Ruby process info array + metricID_UnwindRubyErrNoProcInfo, + + // number of failures to read the stack pointer from the Ruby context + metricID_UnwindRubyErrReadStackPtr, + + // number of failures to read the size of the VM stack from the Ruby context + metricID_UnwindRubyErrReadStackSize, + + // number of failures to read the control frame pointer from the Ruby context + metricID_UnwindRubyErrReadCfp, + + // number of failures to read the expression path from the Ruby frame + metricID_UnwindRubyErrReadEp, + + // number of failures to read the instruction sequence body + metricID_UnwindRubyErrReadIseqBody, + + // number of failures to read the instruction sequence encoded size + metricID_UnwindRubyErrReadIseqEncoded, + + // number of failures to read the instruction sequence size + metricID_UnwindRubyErrReadIseqSize, + + // number of times the unwind instructions requested LR unwinding mid-trace + metricID_UnwindNativeErrLrUnwindingMidTrace, + + // number of failures to read the kernel-mode registers + metricID_UnwindNativeErrReadKernelModeRegs, + + // number of failures to read the IRQ stack link + metricID_UnwindNativeErrChaseIrqStackLink, + + // number of times no entry for a process exists in the V8 process info array + metricID_UnwindV8ErrNoProcInfo, + + // number of times an unwind_info_array index was invalid + metricID_UnwindNativeErrBadUnwindInfoIndex, + + // + // Metric IDs above are for counters (cumulative values) + // + + metricID_BeginCumulative, + + // + // Metric IDs below are for gauges (instantaneous values) + // + + // used as size for maps/metrics (BPF_MAP_TYPE_PERCPU_ARRAY) + metricID_Max +}; + +// TracePrograms provide the offset for each eBPF trace program in the +// map that holds them. +// The values of this enum must fit in a single byte. +typedef enum TracePrograms { + PROG_UNWIND_STOP, + PROG_UNWIND_NATIVE, + PROG_UNWIND_HOTSPOT, + PROG_UNWIND_PERL, + PROG_UNWIND_PYTHON, + PROG_UNWIND_PHP, + PROG_UNWIND_RUBY, + PROG_UNWIND_V8, + NUM_TRACER_PROGS, +} TracePrograms; + +// MAX_FRAME_UNWINDS defines the maximum number of frames per +// Trace we can unwind and respect the limit of eBPF instructions, +// limit of tail calls and limit of stack size per eBPF program. +#define MAX_FRAME_UNWINDS 128 + +// MAX_NON_ERROR_FRAME_UNWINDS defines the maximum number of frames +// to be pushed by unwinders while still leaving space for an error frame. +// This is used to make sure that there is always space for an error +// frame reporting that we ran out of stack space. +#define MAX_NON_ERROR_FRAME_UNWINDS (MAX_FRAME_UNWINDS - 1) + +// Type to represent a globally-unique file id to be used as key for a BPF hash map +typedef u64 FileID; + +// Individual frame in a stack-trace. +typedef struct Frame { + // IDs that uniquely identify a file combination + FileID file_id; + // For PHP this is the line numbers, corresponding to the files in `stack`. + // For Python, each value provides information to allow for the recovery of + // the line number associated with its corresponding offset in `stack`. + // The lower 32 bits provide the co_firstlineno value and the upper 32 bits + // provide the f_lasti value. Other interpreter handlers use the field in + // a similarly domain-specific fashion. + u64 addr_or_line; + // Indicates the type of the frame (Python, PHP, native etc.). + u8 kind; + // 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]; +} Frame; + +_Static_assert(sizeof(Frame) == 3 * 8, "frame padding not working as expected"); + +// TSDInfo contains data needed to extract Thread Specific Data (TSD) values +typedef struct TSDInfo { + s16 offset; + u8 multiplier; + u8 indirect; +} TSDInfo; + +// PerlProcInfo is a container for the data needed to build a stack trace for a Perl process. +typedef struct PerlProcInfo { + u64 stateAddr; + u32 version; + TSDInfo tsdInfo; + // Introspection data + u16 interpreter_curcop, interpreter_curstackinfo; + u8 stateInTSD, si_cxstack, si_next, si_cxix, si_type; + u8 context_type, context_blk_oldcop, context_blk_sub_retop, context_blk_sub_cv, context_sizeof; + u8 sv_flags, sv_any, svu_gp, xcv_flags, xcv_gv, gp_egv; +} PerlProcInfo; + +// PyProcInfo is a container for the data needed to build a stack trace for a Python process. +typedef struct PyProcInfo { + // The address of the autoTLSkey variable + u64 autoTLSKeyAddr; + u16 version; + TSDInfo tsdInfo; + // 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 PyCodeObject_co_argcount, PyCodeObject_co_kwonlyargcount; + u8 PyCodeObject_co_flags, PyCodeObject_co_firstlineno; +} PyProcInfo; + +// PHPProcInfo is a container for the data needed to build a stack trace for a PHP process. +typedef struct PHPProcInfo { + u64 current_execute_data; + // Return address for JIT code (in Hybrid mode) + u64 jit_return_address; + // Offsets for structures we need to access in ebpf + u8 zend_execute_data_function, zend_execute_data_opline, zend_execute_data_prev_execute_data; + 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 { + // The global JIT heap mapping. All JIT code is between these two address. + u64 codecache_start, codecache_end; + + // Offsets of large structures, sizeof it is near or over 256 bytes. + u16 compiledmethod_deopt_handler, nmethod_compileid, nmethod_orig_pc_offset; + + // Offsets and other data fitting in a uchar + u8 codeblob_name; + u8 codeblob_codestart, codeblob_codeend; + u8 codeblob_framecomplete, codeblob_framesize; + u8 heapblock_size, method_constmethod, cmethod_size; + u8 jvm_version, segment_shift; +} HotspotProcInfo; + +// RubyProcInfo is a container for the data needed to build a stack trace for a Ruby process. +typedef struct RubyProcInfo { + // version of the Ruby interpreter. + u32 version; + + // current_ctx_ptr holds the address of the symbol ruby_current_execution_context_ptr. + u64 current_ctx_ptr; + + // Offsets and sizes of Ruby internal structs + + // rb_execution_context_struct offsets: + u8 vm_stack, vm_stack_size, cfp; + + // rb_control_frame_struct offsets: + u8 pc, iseq, ep, size_of_control_frame_struct; + + // rb_iseq_struct offsets: + u8 body; + + // rb_iseq_constant_body: + u8 iseq_type, iseq_encoded, iseq_size; + + // size_of_value holds the size of the macro VALUE as defined in + // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L1136 + u8 size_of_value; + + // rb_ractor_struct offset: + u16 running_ec; + +} RubyProcInfo; + +// V8ProcInfo is a container for the data needed to build a stack trace for a V8 process. +typedef struct V8ProcInfo { + u32 version; + // Introspection data + u16 type_JSFunction_first, type_JSFunction_last, type_Code, type_SharedFunctionInfo; + u8 off_HeapObject_map, off_Map_instancetype, off_JSFunction_code, off_JSFunction_shared; + u8 off_Code_instruction_start, off_Code_instruction_size, off_Code_flags; + u8 fp_marker, fp_function, fp_bytecode_offset; + u8 codekind_shift, codekind_mask, codekind_baseline; +} V8ProcInfo; + +// COMM_LEN defines the maximum length we will receive for the comm of a task. +#define COMM_LEN 16 + +// Container for a stack trace +typedef struct Trace { + // The process ID + u32 pid; + // Monotonic kernel time in nanosecond precision. + u64 ktime; + // The current COMM of the thread of this Trace. + char comm[COMM_LEN]; + // The kernel stack ID. + s32 kernel_stack_id; + // The number of frames in the stack. + u32 stack_len; + // The frames of the stack trace. + Frame frames[MAX_FRAME_UNWINDS]; + + // NOTE: both send_trace in BPF and loadBpfTrace in UM code require `frames` + // to be the last item in the struct. Do not add new members here without also + // adjusting the UM code. +} Trace; + +// Container for unwinding state +typedef struct UnwindState { + // Current register value for Program Counter + u64 pc; + // Current register value for Stack Pointer + u64 sp; + // Current register value for Frame Pointer + u64 fp; + +#if defined(__x86_64__) + // Current register value for r13 + u64 r13; +#elif defined(__aarch64__) + // Current register value for lr + u64 lr; + // Current register value for r22 + u64 r22; +#endif + + // The executable ID/hash associated with PC + u64 text_section_id; + // PC converted into the offset relative to the executables text section + u64 text_section_offset; + // The current mapping load bias + u64 text_section_bias; + + // Unwind error condition to process and report in unwind_stop() + s32 error_metric; + // 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 +} UnwindState; + +// Container for unwinding state needed by the Perl unwinder. Keeping track of +// current stackinfo, first seen COP, and the info about current context stack. +typedef struct PerlUnwindState { + // Pointer to the next stackinfo to unwind + const void *stackinfo; + // First Control OP seen for the frame filename/linenumber info for next function frame + const void *cop; + // Current context state, pointer to the base and current entries + const void *cxbase, *cxcur; +} PerlUnwindState; + +// Container for unwinding state needed by the Python unwinder. At the moment +// the only thing we need to pass between invocations of the unwinding programs +// is the pointer to the next PyFrameObject to unwind. +typedef struct PythonUnwindState { + // Pointer to the next PyFrameObject to unwind + void *py_frame; +} PythonUnwindState; + +// Container for unwinding state needed by the PHP unwinder. At the moment +// the only thing we need to pass between invocations of the unwinding programs +// is the pointer to the next zend_execute_data to unwind. +typedef struct PHPUnwindState { + // Pointer to the next zend_execute_data to unwind + const void *zend_execute_data; +} PHPUnwindState; + +// Container for unwinding state needed by the Ruby unwinder. +typedef struct RubyUnwindState { + // Pointer to the next control frame struct in the Ruby VM stack we want to unwind. + void *stack_ptr; + // Pointer to the last control frame struct in the Ruby VM stack we want to handle. + void *last_stack_frame; +} RubyUnwindState; + +// 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 + // large to be allocated on stack. With my debug build of JDK17, the largest possible variant of + // codeblob that we care about (nmethod) is 376 bytes in size. 512 bytes should thus be plenty. + u8 codeblob[512]; +} HotspotUnwindScratchSpace; + +// The number of bytes read from frame pointer for V8 context +#define V8_FP_CONTEXT_SIZE 64 + +// Container for additional scratch space needed by the V8 unwinder. +typedef struct V8UnwindScratchSpace { + // Read buffer for storing the V8 FP stored context. Needs to be in non-stack + // area to allow variable indexing. + u8 fp_ctx[V8_FP_CONTEXT_SIZE]; + // Read buffer for V8 Code object. Currently we need about 60 bytes to get + // code instruction_size and flags. + u8 code[96]; +} V8UnwindScratchSpace; + +// Container for additional scratch space needed by the Python unwinder. +typedef struct PythonUnwindScratchSpace { + // Read buffer for storing the PyInterpreterFrame (PyFrameObject). + // Python 3.11 is about 80 bytes, but Python 3.7 has larger requirement. + u8 frame[128]; + // Read buffer for storing the PyCodeObject. Currently we need 148 bytes of the header. But + // the structure is 192 bytes in Python 3.11. + u8 code[192]; +} PythonUnwindScratchSpace; + +// Per-CPU info for the stack being built. This contains the stack as well as +// meta-data on the number of eBPF tail-calls used so far to construct it. +typedef struct PerCPURecord { + // The output record, including the stack being built. + Trace trace; + // The current unwind state. + UnwindState state; + // The current Perl unwinder state + PerlUnwindState perlUnwindState; + // The current Python unwinder state. + PythonUnwindState pythonUnwindState; + // The current PHP unwinder state. + PHPUnwindState phpUnwindState; + // The current Ruby unwinder state. + RubyUnwindState rubyUnwindState; + union { + // Scratch space for the HotSpot unwinder. + HotspotUnwindScratchSpace hotspotUnwindScratch; + // Scratch space for the V8 unwinder + V8UnwindScratchSpace v8UnwindScratch; + // Scratch space for the Python unwinder + PythonUnwindScratchSpace pythonUnwindScratch; + }; + // Mask to indicate which unwinders are complete + u32 unwindersDone; + + // tailCalls tracks the number of calls to bpf_tail_call(). + u8 tailCalls; +} PerCPURecord; + +// UnwindInfo contains the unwind information needed to unwind one frame +// from a specific address. +typedef struct UnwindInfo { + u8 opcode; // main opcode to unwind CFA + u8 fpOpcode; // opcode to unwind FP + u8 mergeOpcode; // opcode for generating next stack delta, see below + s32 param; // parameter for the CFA expression + s32 fpParam; // parameter for the FP expression +} UnwindInfo; + +// The 8-bit mergeOpcode consists of two separate fields: +// 1 bit the adjustment to 'param' is negative (-8), if not set positive (+8) +// 7 bits the difference to next 'addrLow' +#define MERGEOPCODE_NEGATIVE 0x80 + +// An array entry that we will bsearch into that keeps address and stack unwind +// info, per executable. +typedef struct StackDelta { + u16 addrLow; // the low 16-bits of the ELF virtual address to which this stack delta applies + u16 unwindInfo; // index of UnwindInfo, or UNWIND_COMMAND_* if STACK_DELTA_COMMAND_FLAG is set +} StackDelta; + +// unwindInfo flag indicating that the value is UNWIND_COMMAND_* value and not an index to +// the unwind info array. When UnwindInfo.opcode is UNWIND_OPCODE_COMMAND the 'param' gives +// the UNWIND_COMMAND_* which describes the exact handling for this stack delta (all +// CFA/PC/FP recovery, or stop condition), and the eBPF code needs special code to handle it. +// This basically serves as a minor optimization to not take a slot from unwind info array, +// nor require a table lookup for these special cased stack deltas. +#define STACK_DELTA_COMMAND_FLAG 0x8000 + +// StackDeltaPageKey is the look up key for stack delta page map. +typedef struct StackDeltaPageKey { + u64 fileID; + u64 page; +} StackDeltaPageKey; + +// StackDeltaPageInfo contains information of stack delta page so the correct map +// and range of StackDelta entries can be found. +typedef struct StackDeltaPageInfo { + u32 firstDelta; + u16 numDeltas; + u16 mapID; +} StackDeltaPageInfo; + + +// Keep stack deltas in 64kB pages to limit search space and to fit the low address +// bits into the addrLow field of struct StackDelta. +#define STACK_DELTA_PAGE_BITS 16 + +// The binary mask for STACK_DELTA_PAGE_BITS, which can be used to and/nand an address +// for its page number and offset within that page. +#define STACK_DELTA_PAGE_MASK ((1 << STACK_DELTA_PAGE_BITS) - 1) + +// In order to determine whether a given PC falls into the main interpreter loop +// of an interpreter, we need to store some data: The lower boundary of the loop, +// the upper boundary of the loop, and the relevant index to call in the prog +// array. +typedef struct OffsetRange { + u64 lower_offset; + u64 upper_offset; + 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 + +// Event is the header for all events sent through the report_events +// perf event output channel (event_send_trigger). +typedef struct Event { + u32 event_type; // EVENT_TYPE_xxx selector of event +} 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 + // longest prefix. + + __be32 pid; // Unique ID of the process. + __be64 page; // Address to a certain part of memory within PID. +} PIDPage; + + +// BIT_WIDTH_PID defines the number of bits used in the value pid of the PIDPage struct. +#define BIT_WIDTH_PID 32 +// BIT_WIDTH_PAGE defines the number of bits used in the value page of the PIDPage struct. +#define BIT_WIDTH_PAGE 64 + +// Constants for accessing bitfields within HotSpot text_section_offset/file_id. +#define HS_TSID_IS_STUB_BIT 63 +#define HS_TSID_HAS_FRAME_BIT 62 +#define HS_TSID_STACK_DELTA_BIT 56 +#define HS_TSID_STACK_DELTA_MASK ((1UL << 6) - 1) +#define HS_TSID_STACK_DELTA_SCALE 8 +#define HS_TSID_SEG_MAP_BIT 0 +#define HS_TSID_SEG_MAP_MASK ((1UL << 56) - 1) + +// PIDPageMappingInfo represents the value of the eBPF map pid_page_to_mapping_info. +typedef struct PIDPageMappingInfo { + u64 file_id; // Unique identifier for the executable file + + // Load bias (7 bytes) + unwinding program to use (1 byte, shifted 7 bytes to the left), encoded in a u64. + // We can do so because the load bias is for userspace addresses, for which the most significant byte is always 0 on + // relevant architectures. + // This encoding may have to be changed if bias can be negative. + u64 bias_and_unwind_program; +} PIDPageMappingInfo; + +// UNKNOWN_FILE indicates for unknown files. +#define UNKNOWN_FILE 0x0 +// FUNC_TYPE_UNKNOWN indicates an unknown interpreted function. +#define FUNC_TYPE_UNKNOWN 0xfffffffffffffffe + +// Builds a bias_and_unwind_program value for PIDPageMappingInfo +static inline __attribute__((__always_inline__)) +u64 encode_bias_and_unwind_program(u64 bias, int unwind_program) { + return bias | (((u64)unwind_program) << 56); +} + +// Reads a bias_and_unwind_program value from PIDPageMappingInfo +static inline __attribute__((__always_inline__)) +void decode_bias_and_unwind_program(u64 bias_and_unwind_program, u64* bias, int* unwind_program) { + *bias = bias_and_unwind_program & 0x00FFFFFFFFFFFFFF; + *unwind_program = bias_and_unwind_program >> 56; +} + +// Smallest stack delta bucket that holds up to 2^8 entries +#define STACK_DELTA_BUCKET_SMALLEST 8 +// Largest stack delta bucket that holds up to 2^21 entries +#define STACK_DELTA_BUCKET_LARGEST 21 + +// Struct of the `system_config` map. Contains various configuration variables +// determined and set by the host agent. +typedef struct SystemConfig { + // PAC mask that is determined by user-space and used in `normalize_pac_ptr`. + // ARM64 specific, `MAX_U64` otherwise. + u64 inverse_pac_mask; + + // The offset of the Thread Pointer Base variable in `task_struct`. It is + // populated by the host agent based on kernel code analysis. + u64 tpbase_offset; + + // Enables the temporary hack that drops pure errors frames in unwind_stop. + bool drop_error_only_traces; +} SystemConfig; + +// Avoid including all of arch/arm64/include/uapi/asm/ptrace.h by copying the +// actually used values. +#define PSR_MODE32_BIT 0x00000010 +#define PSR_MODE_MASK 0x0000000f +#define PSR_MODE_EL0t 0x00000000 + +#endif diff --git a/support/ebpf/v8_tracer.ebpf.c b/support/ebpf/v8_tracer.ebpf.c new file mode 100644 index 00000000..340d4f8a --- /dev/null +++ b/support/ebpf/v8_tracer.ebpf.c @@ -0,0 +1,333 @@ +// This file contains the code and map definitions for the V8 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. The only additional +// task needed in EBPF code is to collect a Code* or SharedFunctionInfo* and potentially +// the current bytecode offset when in interpreted mode. Rest of the processing can +// be done from host agent. +// +// See the host agent interpreterv8.go for more references. + +#undef asm_volatile_goto +#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") + +#include "bpfdefs.h" +#include +#include + +#include "tracemgmt.h" +#include "types.h" +#include "v8_tracer.h" + +#define v8Ver(x,y,z) (((x)<<24)+((y)<<16)+(z)) + +// The number of V8 frames to unwind per frame-unwinding eBPF program. +#define V8_FRAMES_PER_PROGRAM 8 + +// The maximum V8 frame length used in heuristic to validate FP +#define V8_MAX_FRAME_LENGTH 8192 + +// Map from V8 process IDs to a structure containing addresses of variables +// we require in order to build the stack trace +bpf_map_def SEC("maps") v8_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(V8ProcInfo), + .max_entries = 1024, +}; + +// Record a V8 frame +static inline __attribute__((__always_inline__)) +ErrorCode push_v8(Trace *trace, unsigned long pointer_and_type, unsigned long delta_or_marker) { + 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); +} + +// Verify a V8 tagged pointer +static inline __attribute__((__always_inline__)) +uintptr_t v8_verify_pointer(uintptr_t maybe_pointer) { + if ((maybe_pointer & HeapObjectTagMask) != HeapObjectTag) { + return 0; + } + return maybe_pointer & ~HeapObjectTagMask; +} + +// Read and verify a V8 tagged pointer from given memory location. +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)) { + return 0; + } + return v8_verify_pointer(maybe_pointer); +} + +// Verify and parse a V8 SMI ("SMall Integer") value. +// On 64-bit systems: SMI is the upper 32-bits of a 64-bit word, and the lowest bit is the tag. +// Returns the SMI value, or def_value in case of errors. +static inline __attribute__((__always_inline__)) +uintptr_t v8_parse_smi(uintptr_t maybe_smi, uintptr_t def_value) { + if ((maybe_smi & SmiTagMask) != SmiTag) { + return def_value; + } + return maybe_smi >> SmiValueShift; +} + +// Read the type tag of a Heap Object at given memory location. +// Returns zero on error (valid object type IDs are non-zero). +static inline __attribute__((__always_inline__)) +u16 v8_read_object_type(V8ProcInfo *vi, uintptr_t addr) { + if (!addr) { + return 0; + } + 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))) { + return 0; + } + return type; +} + +// Unwind one V8 frame +static inline __attribute__((__always_inline__)) +ErrorCode unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) { + UnwindState *state = &record->state; + Trace *trace = &record->trace; + unsigned long regs[2], sp = state->sp, fp = state->fp, pc = state->pc; + V8UnwindScratchSpace *scratch = &record->v8UnwindScratch; + + // All V8 frames have frame pointer. Check that the FP looks valid. + DEBUG_PRINT("v8: pc: %lx, sp: %lx, fp: %lx", pc, sp, fp); + if (fp < sp || fp >= sp + V8_MAX_FRAME_LENGTH) { + DEBUG_PRINT("v8: frame pointer too far off %lx / %lx", fp, sp); + increment_metric(metricID_UnwindV8ErrBadFP); + return ERR_V8_BAD_FP; + } + + // Read FP pointer data + if (bpf_probe_read(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; + } + + // Make the verifier happy to access fpctx using the HA provided fp_* variables + if (vi->fp_marker > V8_FP_CONTEXT_SIZE - sizeof(unsigned long) || + vi->fp_function > V8_FP_CONTEXT_SIZE - sizeof(unsigned long) || + vi->fp_bytecode_offset > V8_FP_CONTEXT_SIZE - sizeof(unsigned long)) { + return ERR_UNREACHABLE; + } + unsigned long fp_marker = *(unsigned long*)(scratch->fp_ctx + vi->fp_marker); + unsigned long fp_function = *(unsigned long*)(scratch->fp_ctx + vi->fp_function); + unsigned long fp_bytecode_offset = *(unsigned long*)(scratch->fp_ctx + vi->fp_bytecode_offset); + + // Data that will be sent to HA is in these variables. + uintptr_t pointer_and_type = 0, delta_or_marker = 0; + + // Before V8 5.8.261 the frame marker was a SMI. Now it has the tag, but it's not shifted fully. + // The special coding was done to reduce the frame marker push to . + if ((fp_marker & SmiTagMask) == SmiTag) { + // Shift with the tag length only (shift on normal SMI is different). + pointer_and_type = V8_FILE_TYPE_MARKER; + delta_or_marker = fp_marker >> SmiTagShift; + DEBUG_PRINT("v8: -> stub frame, tag %ld", delta_or_marker); + goto frame_done; + } + + // Extract the JSFunction being executed + uintptr_t jsfunc = v8_verify_pointer(fp_function); + u16 jsfunc_tag = v8_read_object_type(vi, jsfunc); + if (jsfunc_tag < vi->type_JSFunction_first || jsfunc_tag > vi->type_JSFunction_last) { + DEBUG_PRINT("v8: -> not a JSFunction: %x <= %x <= %x", + vi->type_JSFunction_first, jsfunc_tag, vi->type_JSFunction_last); + increment_metric(metricID_UnwindV8ErrBadJSFunc); + return ERR_V8_BAD_JS_FUNC; + } + + // Read the SFI to identify the function. + uintptr_t sfi = v8_read_object_ptr(jsfunc + vi->off_JSFunction_shared); + if (v8_read_object_type(vi, sfi) != vi->type_SharedFunctionInfo) { + DEBUG_PRINT("v8: -> no SharedFunctionInfo"); + increment_metric(metricID_UnwindV8ErrBadJSFunc); + return ERR_V8_BAD_JS_FUNC; + } + + // First determine if we are in interpreter mode. The simplest way to check + // is if fp_bytecode_offset holds a SMI (the bytecode delta). The delta is + // relative to the object pointer (not the actual bytecode data), so it is + // always positive. In native mode, the same slot contains a Feedback Vector + // tagged pointer. + delta_or_marker = v8_parse_smi(fp_bytecode_offset, 0); + if (delta_or_marker != 0) { + DEBUG_PRINT("v8: -> bytecode_delta %lx", delta_or_marker); + pointer_and_type = V8_FILE_TYPE_BYTECODE | sfi; + goto frame_done; + } + + // Executing native code. At this point we can at least report the SFI if + // other things fail. + pointer_and_type = V8_FILE_TYPE_NATIVE_SFI | sfi; + + // Try to determine the Code object from JSFunction. + uintptr_t code = v8_read_object_ptr(jsfunc + vi->off_JSFunction_code); + u16 code_type = v8_read_object_type(vi, code); + if (code_type != vi->type_Code) { + // If the object type tag does not match, it might be some new functionality + // in the VM. Report the JSFunction for function name, but report no line + // number information. This allows to get a complete trace even if this one + // frame will have some missing information. + DEBUG_PRINT("v8: jsfunc = %lx, code = %lx, code_type = %x", jsfunc, code, code_type); + increment_metric(metricID_UnwindV8ErrBadCode); + goto frame_done; + } + + // Read the Code blob type and size + if (bpf_probe_read(scratch->code, sizeof(scratch->code), (void*) code)) { + increment_metric(metricID_UnwindV8ErrBadCode); + goto frame_done; + } + // Make the verifier happy to access fpctx using the HA provided fp_* variables + if (vi->off_Code_instruction_size > sizeof(scratch->code) - sizeof(u32) || + vi->off_Code_flags > sizeof(scratch->code) - sizeof(u32)) { + return ERR_UNREACHABLE; + } + + uintptr_t code_start; + if (vi->version >= v8Ver(11, 1, 204)) { + // Starting V8 11.1.204 the instruction/code start is a pointer field instead + // of offset where the code starts. + code_start = *(uintptr_t*)(scratch->code + vi->off_Code_instruction_start); + } else { + code_start = code + vi->off_Code_instruction_start; + } + u32 code_size = *(u32*)(scratch->code + vi->off_Code_instruction_size); + u32 code_flags = *(u32*)(scratch->code + vi->off_Code_flags); + u8 code_kind = (code_flags & vi->codekind_mask) >> vi->codekind_shift; + + uintptr_t code_end = code_start + code_size; + DEBUG_PRINT("v8: func = %lx / sfi = %lx / code = %lx", jsfunc, sfi, code); + DEBUG_PRINT("v8: -> instructions: %lx..%lx (%d)", code_start, code_end, code_size); + + if (!(pc >= code_start && pc < code_end)) { + // PC is not inside the Code object's code area. This can happen due to: + // - on top frame when we are executing prologue/epilogue of called function, + // in this case we can try to recover original PC from the stack + // - the JSFunction's Code object was changed due to On-Stack-Replacement or + // or other deoptimization reasons. This case is currently not handled. + + if (top && trace->stack_len == 0) { + unsigned long stk[3]; + if (bpf_probe_read(stk, sizeof(stk), (void*)(sp - sizeof(stk)))) { + DEBUG_PRINT("v8: --> bad stack pointer"); + increment_metric(metricID_UnwindV8ErrBadFP); + return ERR_V8_BAD_FP; + } + + int i; +#pragma unroll + for (i = sizeof(stk)/sizeof(stk[0])-1; i >= 0; i--) { + if (stk[i] >= code_start && stk[i] < code_end) { + break; + } + } + if (i < 0) { + // Not able to recover PC. + // TODO: investigate why this seems to happen occasionally + DEBUG_PRINT("v8: --> outside code blob: stack top %lx %lx %lx", + stk[2], stk[1], stk[0]); + goto frame_done; + } + + // Recover the PC for the function which is in FP. + pc = stk[i]; + DEBUG_PRINT("v8: --> pc recovered from stack: %lx", pc); + } else { + DEBUG_PRINT("v8: --> outside code blob (not topmost frame)"); + goto frame_done; + } + } + + // Code matches RIP, report it. + if (code_kind == vi->codekind_baseline) { + // Baseline Code does not have backpointer to SFI, so give the JSFunc. + pointer_and_type = V8_FILE_TYPE_NATIVE_JSFUNC | jsfunc; + } else { + pointer_and_type = V8_FILE_TYPE_NATIVE_CODE | code; + } + + // Use cookie that differentiates different types of Code objects + u32 cookie = (code_size << 4) | code_kind; + delta_or_marker = (pc - code_start) | ((uintptr_t)cookie << V8_LINE_COOKIE_SHIFT); + +frame_done: + // Unwind with frame pointer + if (bpf_probe_read(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); + if (error) { + return error; + } + + DEBUG_PRINT("v8: pc: %lx, sp: %lx, fp: %lx", + (unsigned long) state->pc, (unsigned long) state->sp, + (unsigned long) state->fp); + + increment_metric(metricID_UnwindV8Frames); + return ERR_OK; +} + +// unwind_v8 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 +// V8 stack frames to the trace object for the current CPU. +SEC("perf_event/unwind_v8") +int unwind_v8(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_v8 %d ====", trace->stack_len); + + int unwinder = PROG_UNWIND_STOP; + ErrorCode error = ERR_OK; + V8ProcInfo *vi = bpf_map_lookup_elem(&v8_procs, &pid); + if (!vi) { + DEBUG_PRINT("v8: no V8ProcInfo for this pid"); + error = ERR_V8_NO_PROC_INFO; + increment_metric(metricID_UnwindV8ErrNoProcInfo); + goto exit; + } + + increment_metric(metricID_UnwindV8Attempts); + +#pragma unroll + for (int i = 0; i < V8_FRAMES_PER_PROGRAM; i++) { + unwinder = PROG_UNWIND_STOP; + + error = unwind_one_v8_frame(record, vi, i == 0); + if (error) { + break; + } + + error = get_next_unwinder_after_native_frame(record, &unwinder); + if (error || unwinder != PROG_UNWIND_V8) { + break; + } + } + +exit: + record->state.unwind_error = error; + tail_call(ctx, unwinder); + DEBUG_PRINT("v8: tail call for next frame unwinder (%d) failed", unwinder); + return -1; +} diff --git a/support/ebpf/v8_tracer.h b/support/ebpf/v8_tracer.h new file mode 100644 index 00000000..e0df761b --- /dev/null +++ b/support/ebpf/v8_tracer.h @@ -0,0 +1,31 @@ +// This file contains definitions for the V8 tracer + +// V8 constants for the tags. Hard coded to optimize code size and speed. +// They are unlikely to change, and likely require larger modifications on change. + +// https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.2.230/include/v8-internal.h#52 +#define SmiTag 0x0 +// https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.2.230/include/v8-internal.h#54 +#define SmiTagMask 0x1 +// https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.2.230/include/v8-internal.h#91 +#define SmiTagShift 1 +// https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.2.230/include/v8-internal.h#98 +#define SmiValueShift 32 +// https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.2.230/include/v8-internal.h#39 +#define HeapObjectTag 0x1 +// https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.2.230/include/v8-internal.h#42 +#define HeapObjectTagMask 0x3 + +// The Trace 'file' field is split to object pointer (aligned to 8 bytes), +// and the zero bits due to alignment are re-used as the following flags. +#define V8_FILE_TYPE_MARKER 0x0 +#define V8_FILE_TYPE_BYTECODE 0x1 +#define V8_FILE_TYPE_NATIVE_SFI 0x2 +#define V8_FILE_TYPE_NATIVE_CODE 0x3 +#define V8_FILE_TYPE_NATIVE_JSFUNC 0x4 +#define V8_FILE_TYPE_MASK 0x7 + +// The Trace 'line' field is split to two 32-bit fields: cookie and PC-delta +#define V8_LINE_COOKIE_SHIFT 32 +#define V8_LINE_COOKIE_MASK 0xffffffff00000000 +#define V8_LINE_DELTA_MASK 0x00000000ffffffff diff --git a/support/ebpf_integration_test.go b/support/ebpf_integration_test.go new file mode 100644 index 00000000..a46d0379 --- /dev/null +++ b/support/ebpf_integration_test.go @@ -0,0 +1,67 @@ +//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 support + +import ( + "testing" + + cebpf "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + + "github.com/elastic/otel-profiling-agent/libpf/rlimit" +) + +// TestEbpf is a simplified version of otel-profiling-agent. +// It takes the same eBPF ELF file (from support/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) + } + 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) + } + }) + + 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) + } + }) + + 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) + } + }) + + 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) + } + }) +} diff --git a/support/run-tests.sh b/support/run-tests.sh new file mode 100755 index 00000000..a7fcf143 --- /dev/null +++ b/support/run-tests.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Test the current package under a different kernel. +# Requires qemu-system-x86_64 and bluebox to be installed. + +set -eu +set -o pipefail + +color_green=$'\033[32m' +color_red=$'\033[31m' +color_default=$'\033[39m' + +# Use sudo if /dev/kvm isn't accessible by the current user. +sudo="" +if [[ ! -r /dev/kvm || ! -w /dev/kvm ]]; then + sudo="sudo" +fi +readonly sudo + +readonly kernel_version="${1:-}" +if [[ -z "${kernel_version}" ]]; then + echo "Expecting kernel version as first argument" + exit 1 +fi + +readonly kernel="linux-${kernel_version}.bz" +readonly output="$(mktemp -d --suffix=-output)" +readonly kern_dir="${KERN_DIR:-ci-kernels}" + +test -e "${kern_dir}/${kernel}" || { + echo "Failed to find kernel image ${kern_dir}/${kernel}." + exit 1 +} + +echo Generating initramfs +expected=0 +bb_args=(-o "${output}/initramfs.cpio") +while IFS='' read -r -d '' line ; do + bb_args+=(-e "${line}:-test.v") + ((expected=expected+1)) +done < <(find . -name '*.test' -print0) + +additionalQemuArgs="" + +supportKVM=$(grep -c -E 'vmx|svm' /proc/cpuinfo || echo "0") +if [ "$supportKVM" -ne 0 ]; then + additionalQemuArgs="-enable-kvm" +fi + +bluebox "${bb_args[@]}" || (echo "failed to generate initramfs"; exit 1) + +echo Testing on "${kernel_version}" +$sudo qemu-system-x86_64 ${additionalQemuArgs} \ + -nographic \ + -append "console=ttyS0" \ + -monitor none \ + -serial file:"${output}/test.log" \ + -no-user-config \ + -m 4G \ + -kernel "${kern_dir}/${kernel}" \ + -initrd "${output}/initramfs.cpio" + +# Dump the output of the VM run. +cat "${output}/test.log" + +# Qemu will produce an escape sequence that disables line-wrapping in the terminal, +# end result being truncated output. This restores line-wrapping after the fact. +tput smam || true + +passes=$(grep -c "stdout: PASS" "${output}/test.log") + +if [ "$passes" -ne "$expected" ]; then + echo "Test ${color_red}failed${color_default} on ${kernel_version}" + EXIT_CODE=1 +else + echo "Test ${color_green}successful${color_default} on ${kernel_version}" + EXIT_CODE=0 +fi + +$sudo rm -rf "${output}" + +exit $EXIT_CODE diff --git a/support/support.go b/support/support.go new file mode 100644 index 00000000..fef6fc17 --- /dev/null +++ b/support/support.go @@ -0,0 +1,20 @@ +/* + * 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 support + +import ( + "bytes" + + cebpf "github.com/cilium/ebpf" +) + +// LoadCollectionSpec is a wrapper around ebpf.LoadCollectionSpecFromReader and loads the eBPF +// Spec from the embedded file. +// We expect tracerData to hold all possible eBPF maps and programs. +func LoadCollectionSpec() (*cebpf.CollectionSpec, error) { + return cebpf.LoadCollectionSpecFromReader(bytes.NewReader(tracerData)) +} diff --git a/support/support_amd64.go b/support/support_amd64.go new file mode 100644 index 00000000..5b53e0bf --- /dev/null +++ b/support/support_amd64.go @@ -0,0 +1,16 @@ +//go:build amd64 && !dummy + +/* + * 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 support + +import ( + _ "embed" +) + +//go:embed ebpf/tracer.ebpf.x86 +var tracerData []byte diff --git a/support/support_arm64.go b/support/support_arm64.go new file mode 100644 index 00000000..07904444 --- /dev/null +++ b/support/support_arm64.go @@ -0,0 +1,16 @@ +//go:build arm64 && !dummy + +/* + * 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 support + +import ( + _ "embed" +) + +//go:embed ebpf/tracer.ebpf.arm64 +var tracerData []byte diff --git a/support/support_dummy.go b/support/support_dummy.go new file mode 100644 index 00000000..b4231519 --- /dev/null +++ b/support/support_dummy.go @@ -0,0 +1,13 @@ +//go:build dummy + +/* + * 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 support + +// support_dummy.go satisfies build requirements where the eBPF tracers file does not exist. + +var tracerData []byte diff --git a/support/types.go b/support/types.go new file mode 100644 index 00000000..23cc52bc --- /dev/null +++ b/support/types.go @@ -0,0 +1,115 @@ +/* + * 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. + */ + +// support maps the definitions from headers in the C world into a nice go way +package support + +/* +#include +#include "./ebpf/types.h" +#include "./ebpf/frametypes.h" +*/ +import "C" +import "fmt" + +const ( + FrameMarkerUnknown = C.FRAME_MARKER_UNKNOWN + FrameMarkerErrorBit = C.FRAME_MARKER_ERROR_BIT + FrameMarkerPython = C.FRAME_MARKER_PYTHON + FrameMarkerNative = C.FRAME_MARKER_NATIVE + FrameMarkerPHP = C.FRAME_MARKER_PHP + FrameMarkerPHPJIT = C.FRAME_MARKER_PHP_JIT + FrameMarkerKernel = C.FRAME_MARKER_KERNEL + FrameMarkerHotSpot = C.FRAME_MARKER_HOTSPOT + FrameMarkerRuby = C.FRAME_MARKER_RUBY + FrameMarkerPerl = C.FRAME_MARKER_PERL + FrameMarkerV8 = C.FRAME_MARKER_V8 + FrameMarkerAbort = C.FRAME_MARKER_ABORT +) + +const ( + ProgUnwindStop = C.PROG_UNWIND_STOP + ProgUnwindNative = C.PROG_UNWIND_NATIVE + ProgUnwindHotspot = C.PROG_UNWIND_HOTSPOT + ProgUnwindPython = C.PROG_UNWIND_PYTHON + ProgUnwindPHP = C.PROG_UNWIND_PHP + ProgUnwindRuby = C.PROG_UNWIND_RUBY + ProgUnwindPerl = C.PROG_UNWIND_PERL + ProgUnwindV8 = C.PROG_UNWIND_V8 +) + +const ( + DeltaCommandFlag = C.STACK_DELTA_COMMAND_FLAG + + MergeOpcodeNegative = C.MERGEOPCODE_NEGATIVE +) + +const ( + EventTypeGenericPID = C.EVENT_TYPE_GENERIC_PID +) + +const MaxFrameUnwinds = C.MAX_FRAME_UNWINDS + +const ( + MetricIDBeginCumulative = C.metricID_BeginCumulative +) + +const ( + BitWidthPID = C.BIT_WIDTH_PID + BitWidthPage = C.BIT_WIDTH_PAGE +) + +// EncodeBiasAndUnwindProgram encodes a bias_and_unwind_program value (for C.PIDPageMappingInfo) +// from a bias and unwind program values. +// This currently assumes a non-negative bias: this encoding may have to be changed if bias can be +// negative. +func EncodeBiasAndUnwindProgram(bias uint64, + unwindProgram uint8) (uint64, error) { + if (bias >> 56) > 0 { + return 0, fmt.Errorf("unsupported bias value (too large): 0x%x", bias) + } + return bias | (uint64(unwindProgram) << 56), nil +} + +// DecodeBiasAndUnwindProgram decodes the contents of the `bias_and_unwind_program` field in +// C.PIDPageMappingInfo and returns the corresponding bias and unwind program. +func DecodeBiasAndUnwindProgram(biasAndUnwindProgram uint64) (bias uint64, unwindProgram uint8) { + bias = biasAndUnwindProgram & 0x00FFFFFFFFFFFFFF + unwindProgram = uint8(biasAndUnwindProgram >> 56) + 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. + StackDeltaBucketSmallest = C.STACK_DELTA_BUCKET_SMALLEST + StackDeltaBucketLargest = C.STACK_DELTA_BUCKET_LARGEST + + // StackDeltaPage[Bits|Mask] determine the paging size of stack delta map information + StackDeltaPageBits = C.STACK_DELTA_PAGE_BITS + StackDeltaPageMask = C.STACK_DELTA_PAGE_MASK +) + +const ( + HSTSIDIsStubBit = C.HS_TSID_IS_STUB_BIT + HSTSIDHasFrameBit = C.HS_TSID_HAS_FRAME_BIT + HSTSIDStackDeltaBit = C.HS_TSID_STACK_DELTA_BIT + HSTSIDStackDeltaMask = C.HS_TSID_STACK_DELTA_MASK + HSTSIDStackDeltaScale = C.HS_TSID_STACK_DELTA_SCALE + HSTSIDSegMapBit = C.HS_TSID_SEG_MAP_BIT + HSTSIDSegMapMask = C.HS_TSID_SEG_MAP_MASK +) + +const ( + // PerfMaxStackDepth is the bpf map data array length for BPF_MAP_TYPE_STACK_TRACE traces + PerfMaxStackDepth = C.PERF_MAX_STACK_DEPTH +) diff --git a/testsupport/io.go b/testsupport/io.go new file mode 100644 index 00000000..de52692c --- /dev/null +++ b/testsupport/io.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 testsupport + +import ( + "bytes" + "io" + "math/rand" + "testing" +) + +// ValidateReadAtWrapperTransparency validates that a `ReadAt` implementation provides a +// transparent view into the given reference buffer. +func ValidateReadAtWrapperTransparency( + t *testing.T, iterations uint, reference []byte, testee io.ReaderAt) { + bufferSize := uint64(len(reference)) + + // Samples random slices to validate within the file. + r := rand.New(rand.NewSource(0)) // nolint:gosec + for i := uint(0); i < iterations; i++ { + // Intentionally allow slices that over-read the file to test this case. + length := r.Uint64() % bufferSize + start := r.Uint64() % bufferSize + + readBuf := make([]byte, length) + n, err := testee.ReadAt(readBuf, int64(start)) + + truncReadLen := min(bufferSize-start, length) + if truncReadLen != length { + // If we asked to read more than the file has, we expect a truncated read. + if err != io.EOF { + t.Fatalf("expected an EOF error") + } + if uint64(n) != truncReadLen { + t.Fatalf("expected truncation to %d, but got %d", truncReadLen, n) + } + } else { + // Otherwise, we expect a full read. + if uint64(n) != length { + t.Fatalf("read length mismatch (%v vs %v)", n, length) + } + if err != nil { + t.Fatalf("failed to read: %v", err) + } + } + + got := readBuf[:truncReadLen] + expected := reference[start:][:truncReadLen] + if !bytes.Equal(got, expected) { + t.Fatalf("data mismatch: got %v, expected %v", got, expected) + } + } +} + +// GenerateTestInputFile generates a test input file, repeating a number sequence over and over. +func GenerateTestInputFile(seqLen uint8, outputSize uint) []byte { + out := make([]byte, 0, outputSize) + for i := uint(0); i < outputSize; i++ { + out = append(out, byte(i%uint(seqLen))) + } + + return out +} diff --git a/testsupport/testfiles.go b/testsupport/testfiles.go new file mode 100644 index 00000000..57f787df --- /dev/null +++ b/testsupport/testfiles.go @@ -0,0 +1,904 @@ +/* + * 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 testsupport + +import ( + "encoding/base64" + "fmt" + "os" +) + +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?") + } + exeFile, err := os.CreateTemp("", "proc_test_tmp_exe_*") + if err != nil { + return "", fmt.Errorf("failed to open tempfile") + } + + b, err := exeFile.Write(buffer) + if b != len(buffer) { + return "", fmt.Errorf("failed to write file (wrote %d bytes instead of %d)", b, + len(buffer)) + } else if err != nil { + return "", fmt.Errorf("failed to write file: %v", err) + } + exeFile.Close() + + return exeFile.Name(), nil +} + +// WriteTestExecutable1 writes a position independent executable to disk and +// returns its path +func WriteTestExecutable1() (string, error) { + return writeExecutable(usrBinDumpmscat) +} + +// WriteTestExecutable2 writes a non- position independent executable to disk +// and returns its path +func WriteTestExecutable2() (string, error) { + return writeExecutable(helloworldBin) +} + +// WriteSharedLibrary writes a shared library to disk and returns its path +func WriteSharedLibrary() (string, error) { + return writeExecutable(sharedlib) +} + +// Base64 encoded version of /usr/bin/dumpmscat from Fedora 30. It is a PIE. +// The symbol 'main' is at 0xfc0 and the symbol '_start' is at '0x1410'. +var usrBinDumpmscat = `f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAEBQAAAAAAABAAAAAAAAAAMg2AAAAAAAAAAAAAEAAOAAJ +AEAAIQAgAAYAAAAEAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAA+AEAAAAAAAD4AQAAAAAAAAgA +AAAAAAAAAwAAAAQAAAA4AgAAAAAAADgCAAAAAAAAOAIAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA +AAAAAAABAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAYAAAAAAAAgBgAAAAAAAAAEAAA +AAAAAAEAAAAGAAAAsBsAAAAAAACwKwAAAAAAALArAAAAAAAAWAQAAAAAAABZBAAAAAAAAAAQAAAA +AAAAAgAAAAYAAADIGwAAAAAAAMgrAAAAAAAAyCsAAAAAAABgAwAAAAAAAGADAAAAAAAACAAAAAAA +AAAEAAAABAAAAFQCAAAAAAAAVAIAAAAAAABUAgAAAAAAAGQAAAAAAAAAZAAAAAAAAAAEAAAAAAAA +AFDldGQEAAAATBgAAAAAAABMGAAAAAAAAEwYAAAAAAAANAAAAAAAAAA0AAAAAAAAAAQAAAAAAAAA +UeV0ZAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABS +5XRkBgAAALAbAAAAAAAAsCsAAAAAAACwKwAAAAAAAFAEAAAAAAAAUAQAAAAAAAAIAAAAAAAAAC9s +aWI2NC9sZC1saW51eC14ODYtNjQuc28uMgAEAAAAEAAAAAEAAABHTlUAAAAAAAMAAAACAAAAAAAA +AAQAAAAQAAAABQAAAEdOVQACAADABAAAAAMAAAAAAAAABAAAABQAAAADAAAAR05VAGkg/SF6hBYT +H0N37wGKLJMvMRttAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAABIAAAAAAAAAAAAAAAAAAAAA +AAAAQQAAABIAAAAAAAAAAAAAAAAAAAAAAAAARgAAABIAAAAAAAAAAAAAAAAAAAAAAAAAbgAAABIA +AAAAAAAAAAAAAAAAAAAAAAAAhwAAABIAAAAAAAAAAAAAAAAAAAAAAAAAjwAAABIAAAAAAAAAAAAA +AAAAAAAAAAAAwAAAABIAAAAAAAAAAAAAAAAAAAAAAAAA1QAAABIAAAAAAAAAAAAAAAAAAAAAAAAA +8wAAABIAAAAAAAAAAAAAAAAAAAAAAAAADgEAABIAAAAAAAAAAAAAAAAAAAAAAAAAHQEAABIAAAAA +AAAAAAAAAAAAAAAAAAAAOAEAABIAAAAAAAAAAAAAAAAAAAAAAAAAUAEAABIAAAAAAAAAAAAAAAAA +AAAAAAAAYQEAABIAAAAAAAAAAAAAAAAAAAAAAAAAdAEAABIAAAAAAAAAAAAAAAAAAAAAAAAAjwEA +ACIAAAAAAAAAAAAAAAAAAAAAAAAAngEAABIAAAAAAAAAAAAAAAAAAAAAAAAAqwEAABIAAAAAAAAA +AAAAAAAAAAAAAAAAsgEAABEAAAAAAAAAAAAAAAAAAAAAAAAAuQEAABIAAAAAAAAAAAAAAAAAAAAA +AAAAvgEAACAAAAAAAAAAAAAAAAAAAAAAAAAA2AEAACAAAAAAAAAAAAAAAAAAAAAAAAAA9AEAACAA +AAAAAAAAAAAAAAAAAAAAAAAAAQAAABAAEwAJMAAAAAAAAAAAAAAAAAAAEgAAABAAEwAIMAAAAAAA +AAAAAAAAAAAAAwIAABAAGQAAMAAAAAAAAAAAAAAAAAAABgAAABAAEwAIMAAAAAAAAAAAAAAAAAAA +EAIAABEAEACIFQAAAAAAAAQAAAAAAAAAHwIAACAAGQAAMAAAAAAAAAAAAAAAAAAAKgIAABIADgDA +DwAAAAAAAFAEAAAAAAAALwIAABIADgAAFQAAAAAAAGUAAAAAAAAAPwIAABIADgAQFAAAAAAAAC8A +AAAAAAAARgIAABIADgBwFQAAAAAAAAUAAAAAAAAAAF9lbmQAX19ic3Nfc3RhcnQAX2VkYXRhAF9f +bGliY19zdGFydF9tYWluAEdMSUJDXzIuMi41AGxpYmMuc28uNgBwdXRzAHRhbGxvY19pbml0AFRB +TExPQ18yLjAuMgBsaWJ0YWxsb2Muc28uMgBfX3ByaW50Zl9jaGsAR0xJQkNfMi4zLjQAcHV0Y2hh +cgBtc2NhdF9jdGxfaW1wb3J0AFNBTUJBXzQuMTAuOABsaWJtc2NhdC1zYW1iYTQuc28AbXNjYXRf +Y3RsX2dldF9tZW1iZXIAbXNjYXRfY3RsX2dldF9hdHRyaWJ1dGVfY291bnQAbXNjYXRfcGtjczdf +aW1wb3J0X2NhdGZpbGUAbXNjYXRfY3RsX2luaXQAbXNjYXRfY3RsX2dldF9tZW1iZXJfY291bnQA +bXNjYXRfY3RsX2dldF9hdHRyaWJ1dGUAbXNjYXRfcGtjczdfaW5pdABtc2NhdF9wa2NzN192ZXJp +ZnkAX19zdGFja19jaGtfZmFpbABHTElCQ18yLjQAX19jeGFfZmluYWxpemUAX3RhbGxvY19mcmVl +AGZ3cml0ZQBzdGRlcnIAZXhpdABfSVRNX3JlZ2lzdGVyVE1DbG9uZVRhYmxlAF9JVE1fZGVyZWdp +c3RlclRNQ2xvbmVUYWJsZQBfX2dtb25fc3RhcnRfXwBfX2RhdGFfc3RhcnQAX0lPX3N0ZGluX3Vz +ZWQAZGF0YV9zdGFydABtYWluAF9fbGliY19jc3VfaW5pdABfc3RhcnQAX19saWJjX2NzdV9maW5p +AGxpYnJlcGxhY2Utc2FtYmE0LnNvAGxpYnNhbWJhLXV0aWwuc28uMABsaWJnZW5yYW5kLXNhbWJh +NC5zbwBsaWJzb2NrZXQtYmxvY2tpbmctc2FtYmE0LnNvAGxpYmFlc25pLWludGVsLXNhbWJhNC5z +bwBsaWJ1dGlsLXNldGlkLXNhbWJhNC5zbwBsaWJzYW1iYS1kZWJ1Zy1zYW1iYTQuc28AbGlidGlt +ZS1iYXNpYy1zYW1iYTQuc28AbGlic3lzLXJ3LXNhbWJhNC5zbwBsaWJpb3YtYnVmLXNhbWJhNC5z +bwBsaWJuc2wuc28uMgBsaWJkbC5zby4yAGxpYnJ0LnNvLjEAbGlidGV2ZW50LnNvLjAAbGliY3J5 +cHQuc28uMgBsaWJwdGhyZWFkLnNvLjAAbGlic3lzdGVtZC5zby4wAGxpYmdudXRscy5zby4zMABs +aWJ0YXNuMS5zby42AC91c3IvbGliNjQvc2FtYmEAAAAAAAAAAAMAAAAYAAAAAQAAAAYAAACIUSEF +IGWEKBgAAAAbAAAAIQAAALrjknxCRdXsawlD1thxWBysS+PADDqXC2p/mnzazeOeM2Lb7Q0U4p4A +AAIAAgAFAAMAAgAGAAYABgAGAAYABgAGAAYABgAEAAIABQACAAIAAgAAAAAAAAABAAEAAQABAAEA +AQABAAEAAQABAAEAAwA3AAAAEAAAAEAAAAB1GmkJAAACACsAAAAQAAAAdBlpCQAAAwB7AAAAEAAA +ABRpaQ0AAAQAhQEAAAAAAAABAAEAXwAAABAAAAAgAAAAIgSjDgAABQBSAAAAAAAAAAEAAQCtAAAA +EAAAAAAAAABoVScHAAAGAKAAAAAAAAAAsCsAAAAAAAAIAAAAAAAAALArAAAAAAAAuCsAAAAAAAAI +AAAAAAAAALAUAAAAAAAAwCsAAAAAAAAIAAAAAAAAAPAUAAAAAAAAKC8AAAAAAAAGAAAAAQAAAAAA +AAAAAAAASC8AAAAAAAAGAAAAEAAAAAAAAAAAAAAAUC8AAAAAAAAGAAAAEwAAAAAAAAAAAAAAQC8A +AAAAAAAGAAAAFQAAAAAAAAAAAAAAOC8AAAAAAAAGAAAAFgAAAAAAAAAAAAAAMC8AAAAAAAAGAAAA +FwAAAAAAAAAAAAAAcC8AAAAAAAAHAAAAEAAAAAAAAAAAAAAAeC8AAAAAAAAHAAAAAwAAAAAAAAAA +AAAAgC8AAAAAAAAHAAAADQAAAAAAAAAAAAAAiC8AAAAAAAAHAAAACQAAAAAAAAAAAAAAkC8AAAAA +AAAHAAAADgAAAAAAAAAAAAAAmC8AAAAAAAAHAAAAAgAAAAAAAAAAAAAAoC8AAAAAAAAHAAAACgAA +AAAAAAAAAAAAqC8AAAAAAAAHAAAABgAAAAAAAAAAAAAAsC8AAAAAAAAHAAAACwAAAAAAAAAAAAAA +uC8AAAAAAAAHAAAABAAAAAAAAAAAAAAAwC8AAAAAAAAHAAAABQAAAAAAAAAAAAAAyC8AAAAAAAAH +AAAABwAAAAAAAAAAAAAA0C8AAAAAAAAHAAAACAAAAAAAAAAAAAAA2C8AAAAAAAAHAAAADAAAAAAA +AAAAAAAA4C8AAAAAAAAHAAAAEQAAAAAAAAAAAAAA6C8AAAAAAAAHAAAAFAAAAAAAAAAAAAAA8C8A +AAAAAAAHAAAAEgAAAAAAAAAAAAAA+C8AAAAAAAAHAAAADwAAAAAAAAAAAAAA8w8e+kiD7AhIiwXR +IQAASIXAdAL/0EiDxAjDAAAAAAD/NeohAADy/yXrIQAADx8A8w8e+mgAAAAA8unh////kPMPHvpo +AQAAAPLp0f///5DzDx76aAIAAADy6cH///+Q8w8e+mgDAAAA8umx////kPMPHvpoBAAAAPLpof// +/5DzDx76aAUAAADy6ZH///+Q8w8e+mgGAAAA8umB////kPMPHvpoBwAAAPLpcf///5DzDx76aAgA +AADy6WH///+Q8w8e+mgJAAAA8ulR////kPMPHvpoCgAAAPLpQf///5DzDx76aAsAAADy6TH///+Q +8w8e+mgMAAAA8ukh////kPMPHvpoDQAAAPLpEf///5DzDx76aA4AAADy6QH///+Q8w8e+mgPAAAA +8unx/v//kPMPHvpoEAAAAPLp4f7//5DzDx76aBEAAADy6dH+//+Q8w8e+vL/JcUgAAAPHwQAkPMP +Hvry/yW9IAAADx8EAJDzDx768v8ltSAAAA8fBACQ8w8e+vL/Ja0gAAAPHwQAkPMPHvry/yWlIAAA +Dx8EAJDzDx768v8lnSAAAA8fBACQ8w8e+vL/JZUgAAAPHwQAkPMPHvry/yWNIAAADx8EAJDzDx76 +8v8lhSAAAA8fBACQ8w8e+vL/JX0gAAAPHwQAkPMPHvry/yV1IAAADx8EAJDzDx768v8lbSAAAA8f +BACQ8w8e+vL/JWUgAAAPHwQAkPMPHvry/yVdIAAADx8EAJDzDx768v8lVSAAAA8fBACQ8w8e+vL/ +JU0gAAAPHwQAkPMPHvry/yVFIAAADx8EAJDzDx768v8lPSAAAA8fBACQ8w8e+kFXQVZBVUFUVVNI +g+w4ZEiLBCUoAAAASIlEJCgxwIX/D47UAwAATIt2CEmJ9U2F9g+ExAMAAEGAPgAPhLoDAACJ+0iN +PaMFAADonv7//0mJxEiFwA+EwwMAAEiJx+ia/v//SInFSIXAD4SlAwAATIn2SInH6JP+//+FwA+F +kgMAADH2g/sBdARJi3UQSInv6Ij+//+FwA+FawMAAEiNPXAFAADohP7//0yJ5+iM/v//SYnFSIXA +D4RXAwAASInuSInH6IX+//+JRCQMhcAPhUADAABMie8x7UyNNa0FAADoeP7//0iNNUAFAAC/AQAA +AEiNHWkGAACJRCQIicJBiccxwOhl/v//SI1EJBhIiQQkRYX/dRvpZwEAAGaQvwoAAADoVv7//zts +JAgPhFEBAACDxQFIiwwkTInmTInvieroSP7//4XAD4XHAgAASI09DQUAAOjU/f//SItUJBiLAoP4 +AQ+E5AEAAIP4Ag+E+wEAAL8KAAAA6AH+//9Ii0QkGEiLUBhIhdJ0G4tIIEiNNfwEAAC/AQAAADHA +6M39//9Ii0QkGEiLUDhIhdJ0G4tIQEiNNfIEAAC/AQAAADHA6Kn9//9Ii0QkGEiLUChIhdJ0G4tI +MEiNNeUEAAC/AQAAADHA6IX9//9Ii0QkGItASIXAD4Ql////g/gFD4fuAQAASGMEg0gB2D7/4EiN +FdcDAAAxwEiNNcIEAAC/AQAAAEUx/+hF/f//SItEJBhIg3hYAHQwZg8fhAAAAAAASItAUEyJ9r8B +AAAAQg+2FDgxwEmDxwHoFP3//0iLRCQYTDl4WHfZvwoAAADoD/3//78KAAAA6AX9//87bCQID4Wv +/v//vwoAAABFMf9IjWwkIOjp/P//TInvTI01nwQAAOj6/P//SI01mwMAAL8BAAAAicKJwzHA6LP8 +//+F23RHDx+AAAAAAEGDxwFIielMieZMie9Eifro0/z//4XAD4UyAQAASItEJCBMifa/AQAAAItI +CEyLQBBIixAxwOht/P//QTnfdcBIjTURBAAATInn6Kn8//9Ii0QkKGRIMwQlKAAAAA+FIAEAAItE +JAxIg8Q4W11BXEFdQV5BX8NIjRWPAgAA6cv+//9mDx9EAABIi1IISI01GAMAAL8BAAAAMcDoCfz/ +/+kJ/v//Dx9AADHASI01CgMAAL8BAAAARTH/6Or7//9Ii0QkGEiDeBAAdC1mDx9EAABIi0AITIn2 +vwEAAABCD7YUODHASYPHAei8+///SItEJBhMOXgQd9m/CgAAAOi3+///6af9//9IjRUQAgAA6Tv+ +//9IjRX9AQAA6S/+//9IjRXsAQAA6SP+//9IjRXTAQAA6Rf+///HRCQM/////+kM////SI095gIA +AOgZ+///vwEAAADor/v//0iLBWgbAAC6HAAAAL4BAAAASI09wQEAAEiLCOif+///vwEAAADohfv/ +/+ig+///8w8e+jHtSYnRXkiJ4kiD5PBQVEyNBUYBAABIjQ3PAAAASI09iPv///8V6hoAAPSQSI09 +wRsAAEiNBbobAABIOfh0FUiLBd4aAABIhcB0Cf/gDx+AAAAAAMMPH4AAAAAASI09kRsAAEiNNYob +AABIKf5IifBIwe4/SMH4A0gBxkjR/nQUSIsFpRoAAEiFwHQI/+BmDx9EAADDDx+AAAAAAPMPHvqA +PU0bAAAAdStVSIM9ghoAAABIieV0DEiNPd4WAADoyfn//+hk////xgUlGwAAAV3DDx8Aww8fgAAA +AADzDx766Xf///8PH4AAAAAA8w8e+kFXTI09sxYAAEFWSYnWQVVJifVBVEGJ/FVIjS2kFgAAU0wp +/UiD7AjoH/j//0jB/QN0HzHbDx+AAAAAAEyJ8kyJ7kSJ50H/FN9Ig8MBSDnddepIg8QIW11BXEFd +QV5BX8NmZi4PH4QAAAAAAPMPHvrDAAAA8w8e+kiD7AhIg8QIwwAAAAEAAgBVTktOT1dOAE5VTEwA +U0hBMQBTSEEyNTYAU0hBNTEyAE1ENQBkdW1wbXNjYXQARmFpbGVkIHRvIGluaXRpYWxpemUgdGFs +bG9jCgBDQVRBTE9HIEZJTEUgVkVSSUZJRUQhCgBDQVRBTE9HIE1FTUJFUiBDT1VOVD0lZAoAQ0FU +QUxPRyBBVFRSSUJVVEUgQ09VTlQ9JWQKAENBVEFMT0cgTUVNQkVSACAgQ0hFQ0tTVU06ICVzCgAg +IENIRUNLU1VNOiAAJVgAICBGSUxFOiAlcywgRkxBR1M9MHglMDh4CgAgIEdVSUQ6ICVzLCBJRD0w +eCUwOHgKACAgT1NBVFRSOiAlcywgRkxBR1M9MHglMDh4CgAgIE1BQzogJXMsIERJR0VTVDogAAAA +AABGQUlMRUQgVE8gVkVSSUZZIENBVEFMT0cgRklMRSEAAAAAAAAAAAAALi4vLi4vbGliL21zY2F0 +L2R1bXBtc2NhdC5jOjE4NgAgIE5BTUU9JXMsIEZMQUdTPTB4JTA4eCwgVkFMVUU9JXMKAACO/P// +2vv//6r6//+C/P//dvz//2r8//8AAAAAFAAAAAAAAAABelIAAXgQARsMBwiQAQAAFAAAABwAAACw +/P//LwAAAABEBxAAAAAATAAAADQAAABI+P//UAQAAABGDhCPAkIOGI4DQg4gjQRCDiiMBUEOMIYG +QQ44gwdEDnADIQMKDjhBDjBBDihCDiBCDhhCDhBCDghBCwAAAABEAAAAhAAAADj9//9lAAAAAEYO +EI8CSQ4YjgNFDiCNBEUOKIwFRA4whgZIDjiDB0cOQG4OOEEOMEEOKEIOIEIOGEIOEEIOCAAUAAAA +zAAAAGD9//8FAAAAAAAAAAAAAAAkAAAA5AAAAEj1//9QAgAAAA4QRg4YSg8LdwiAAD8aOSozJCIA +AAAAAAAAAAEbAzvw/v//BQAAACT1///U////dPf//yT////E+///DP///7T8//90////JP3//7z/ +//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAACwKwAAAAAAALAUAAAAAAAA8BQAAAAAAAADAAAAAAAAAFgvAAAA +AAAAAgAAAAAAAACwAQAAAAAAABcAAAAAAAAAoAsAAAAAAAAUAAAAAAAAAAcAAAAAAAAABwAAAAAA +AADICgAAAAAAAAgAAAAAAAAA2AAAAAAAAAAJAAAAAAAAABgAAAAAAAAA+f//bwAAAAADAAAAAAAA +ABUAAAAAAAAAAAAAAAAAAAAGAAAAAAAAALgCAAAAAAAACwAAAAAAAAAYAAAAAAAAAAUAAAAAAAAA +6AUAAAAAAAAKAAAAAAAAAMkDAAAAAAAA9f7/bwAAAAC4CQAAAAAAAAEAAAAAAAAArQAAAAAAAAAB +AAAAAAAAAFYCAAAAAAAAAQAAAAAAAABrAgAAAAAAAAEAAAAAAAAAfgIAAAAAAAABAAAAAAAAAJMC +AAAAAAAAAQAAAAAAAACwAgAAAAAAAAEAAAAAAAAAyQIAAAAAAAABAAAAAAAAAOECAAAAAAAAAQAA +AAAAAAD6AgAAAAAAAAEAAAAAAAAAEgMAAAAAAAABAAAAAAAAACYDAAAAAAAAAQAAAAAAAAA7AwAA +AAAAAAEAAAAAAAAARwMAAAAAAAABAAAAAAAAAFIDAAAAAAAAAQAAAAAAAABdAwAAAAAAAAEAAAAA +AAAAXwAAAAAAAAABAAAAAAAAAGwDAAAAAAAAAQAAAAAAAAB6AwAAAAAAAAEAAAAAAAAAigMAAAAA +AAABAAAAAAAAAJoDAAAAAAAAAQAAAAAAAACqAwAAAAAAAAEAAAAAAAAANwAAAAAAAAAMAAAAAAAA +AFANAAAAAAAADQAAAAAAAAB4FQAAAAAAABoAAAAAAAAAuCsAAAAAAAAcAAAAAAAAAAgAAAAAAAAA +GQAAAAAAAADAKwAAAAAAABsAAAAAAAAACAAAAAAAAAAdAAAAAAAAALgDAAAAAAAAHgAAAAAAAAAI +AAAAAAAAAPv//28AAAAAAQAAAAAAAADw//9vAAAAAAQKAAAAAAAA/v//bwAAAABICgAAAAAAAP// +/28AAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADIKwAAAAAAAAAAAAAA +AAAAAAAAAAAAAACADQAAAAAAAJANAAAAAAAAoA0AAAAAAACwDQAAAAAAAMANAAAAAAAA0A0AAAAA +AADgDQAAAAAAAPANAAAAAAAAAA4AAAAAAAAQDgAAAAAAACAOAAAAAAAAMA4AAAAAAABADgAAAAAA +AFAOAAAAAAAAYA4AAAAAAABwDgAAAAAAAIAOAAAAAAAAkA4AAAAAAAAAAAAAAAAAAAoAAAAQAAAA +AAEAAEdBJAEzaDg3MQAAABAUAAAAAAAAEBQAAAAAAAAKAAAAEAAAAAABAABHQSQBM2M4NzEAAADA +DwAAAAAAAMAPAAAAAAAACgAAABAAAAAAAQAAR0EkATNzODcxAAAAwA8AAAAAAADADwAAAAAAAAoA +AAAQAAAAAAEAAEdBJAEzZTg3MQAAAMAPAAAAAAAAwA8AAAAAAAAIAAAAEAAAAAABAABHQSQBM2Ex +ABAUAAAAAAAAPxQAAAAAAAAIAAAAEAAAAAABAABHQSQBM2ExAD8UAAAAAAAAPxQAAAAAAAAKAAAA +EAAAAAABAABHQSQBM3A4NzEAAAA/FAAAAAAAAD8UAAAAAAAAFwAAAAAAAAAAAQAAR0EkBWdjYyA5 +LjEuMSAyMDE5MDUwMwAACgAAAAAAAAAAAQAAR0EqR09XAKpEAAAABgAAAAAAAAAAAQAAR0EqAgEA +AAAPAAAAAAAAAAABAABHQStzdGFja19jbGFzaAAAEwAAAAAAAAAAAQAAR0EqY2ZfcHJvdGVjdGlv +bgACAAANAAAAAAAAAAABAABHQSpGT1JUSUZZAP8AAAAAFgAAAAAAAAAAAQAAR0ErR0xJQkNYWF9B +U1NFUlRJT05TAAAABgAAAAAAAAAAAQAAR0EqBwMAAAAFAAAAAAAAAAABAABHQSEIAAAAABYAAAAA +AAAAAAEAAEdBIW9taXRfZnJhbWVfcG9pbnRlcgAAAAwAAAAAAAAAAAEAAEdBKgYSAAAAEQAMABEA +AAAAAAAAAAEAAEdBIXN0YWNrX3JlYWxpZ24AAAAAFwAAAAAAAAAAAQAAR0EkBWdjYyA5LjEuMSAy +MDE5MDUwMwAACgAAAAAAAAAAAQAAR0EqR09XAKpEAAAABgAAAAAAAAAAAQAAR0EqAgEAAAAPAAAA +AAAAAAABAABHQStzdGFja19jbGFzaAAAEwAAAAAAAAAAAQAAR0EqY2ZfcHJvdGVjdGlvbgACAAAN +AAAAAAAAAAABAABHQSpGT1JUSUZZAP8AAAAAFgAAAAAAAAAAAQAAR0ErR0xJQkNYWF9BU1NFUlRJ +T05TAAAABgAAAAAAAAAAAQAAR0EqBwMAAAAFAAAAAAAAAAABAABHQSEIAAAAABYAAAAAAAAAAAEA +AEdBIW9taXRfZnJhbWVfcG9pbnRlcgAAAAwAAAAAAAAAAAEAAEdBKgYSAAAAEQAMABEAAAAAAAAA +AAEAAEdBIXN0YWNrX3JlYWxpZ24AAAAAFwAAAAAAAAAAAQAAR0EkBWdjYyA5LjEuMSAyMDE5MDUw +MwAACgAAAAAAAAAAAQAAR0EqR09XAKpEAAAABgAAAAAAAAAAAQAAR0EqAgEAAAAPAAAAAAAAAAAB +AABHQStzdGFja19jbGFzaAAAEwAAAAAAAAAAAQAAR0EqY2ZfcHJvdGVjdGlvbgACAAANAAAAAAAA +AAABAABHQSpGT1JUSUZZAP8AAAAAFgAAAAAAAAAAAQAAR0ErR0xJQkNYWF9BU1NFUlRJT05TAAAA +BgAAAAAAAAAAAQAAR0EqBwMAAAAFAAAAAAAAAAABAABHQSEIAAAAABYAAAAAAAAAAAEAAEdBIW9t +aXRfZnJhbWVfcG9pbnRlcgAAAAwAAAAAAAAAAAEAAEdBKgYSAAAAEQAMABEAAAAAAAAAAAEAAEdB +IXN0YWNrX3JlYWxpZ24AAAAAFwAAAAAAAAAAAQAAR0EkBWdjYyA5LjEuMSAyMDE5MDUwMwAACgAA +AAAAAAAAAQAAR0EqR09XAKpEAAAABgAAAAAAAAAAAQAAR0EqAgEAAAAPAAAAAAAAAAABAABHQStz +dGFja19jbGFzaAAAEwAAAAAAAAAAAQAAR0EqY2ZfcHJvdGVjdGlvbgACAAANAAAAAAAAAAABAABH +QSpGT1JUSUZZAP8AAAAAFgAAAAAAAAAAAQAAR0ErR0xJQkNYWF9BU1NFUlRJT05TAAAABgAAAAAA +AAAAAQAAR0EqBwMAAAAFAAAAAAAAAAABAABHQSEIAAAAABYAAAAAAAAAAAEAAEdBIW9taXRfZnJh +bWVfcG9pbnRlcgAAAAwAAAAAAAAAAAEAAEdBKgYSAAAAEQAMABEAAAAAAAAAAAEAAEdBIXN0YWNr +X3JlYWxpZ24AAAAAFwAAAAAAAAAAAQAAR0EkBWdjYyA5LjEuMSAyMDE5MDUwMwAACgAAAAAAAAAA +AQAAR0EqR09XAKpEAAAABgAAAAAAAAAAAQAAR0EqAgEAAAAPAAAAAAAAAAABAABHQStzdGFja19j +bGFzaAAAEwAAAAAAAAAAAQAAR0EqY2ZfcHJvdGVjdGlvbgACAAANAAAAAAAAAAABAABHQSpGT1JU +SUZZAP8AAAAAFgAAAAAAAAAAAQAAR0ErR0xJQkNYWF9BU1NFUlRJT05TAAAABgAAAAAAAAAAAQAA +R0EqBwMAAAAFAAAAAAAAAAABAABHQSEIAAAAABYAAAAAAAAAAAEAAEdBIW9taXRfZnJhbWVfcG9p +bnRlcgAAAAwAAAAAAAAAAAEAAEdBKgYSAAAAEQAMABEAAAAAAAAAAAEAAEdBIXN0YWNrX3JlYWxp +Z24AAAAACAAAABAAAAAAAQAAR0EkATNhMQA/FAAAAAAAAD8UAAAAAAAACAAAABAAAAAAAQAAR0Ek +ATNhMQBQDQAAAAAAAGYNAAAAAAAACAAAABAAAAAAAQAAR0EkATNhMQB4FQAAAAAAAIAVAAAAAAAA +CAAAABAAAAAAAQAAR0EkATNhMQBAFAAAAAAAAPkUAAAAAAAACgAAABAAAAAAAQAAR0EkATNwODcx +AAAA+RQAAAAAAAD5FAAAAAAAABcAAAAAAAAAAAEAAEdBJAVnY2MgOS4yLjEgMjAxOTA4MjcAAAoA +AAAAAAAAAAEAAEdBKkdPVwCqRAAAAAYAAAAAAAAAAAEAAEdBKgIBAAAADwAAAAAAAAAAAQAAR0Er +c3RhY2tfY2xhc2gAABMAAAAAAAAAAAEAAEdBKmNmX3Byb3RlY3Rpb24AAgAADQAAAAAAAAAAAQAA +R0EqRk9SVElGWQACAAAAABYAAAAAAAAAAAEAAEdBK0dMSUJDWFhfQVNTRVJUSU9OUwAAAAYAAAAA +AAAAAAEAAEdBKgcCAAAABQAAAAAAAAAAAQAAR0EhCAAAAAAWAAAAAAAAAAABAABHQSFvbWl0X2Zy +YW1lX3BvaW50ZXIAAAAMAAAAAAAAAAABAABHQSoGEgAAABEADAARAAAAAAAAAAABAABHQSFzdGFj +a19yZWFsaWduAAAAAAoAAAAQAAAAAAEAAEdBJAEzaDg3MQAAABAUAAAAAAAAEBQAAAAAAAAXAAAA +AAAAAAABAABHQSQFZ2NjIDkuMi4xIDIwMTkwODI3AAAKAAAAAAAAAAABAABHQSpHT1cAqkQAAAAG +AAAAAAAAAAABAABHQSoCAQAAAA8AAAAAAAAAAAEAAEdBK3N0YWNrX2NsYXNoAAATAAAAAAAAAAAB +AABHQSpjZl9wcm90ZWN0aW9uAAIAAA0AAAAAAAAAAAEAAEdBKkZPUlRJRlkAAgAAAAAWAAAAAAAA +AAABAABHQStHTElCQ1hYX0FTU0VSVElPTlMAAAAGAAAAAAAAAAABAABHQSoHAgAAAAUAAAAAAAAA +AAEAAEdBIQgAAAAAFgAAAAAAAAAAAQAAR0Ehb21pdF9mcmFtZV9wb2ludGVyAAAADAAAAAAAAAAA +AQAAR0EqBhIAAAARAAwAEQAAAAAAAAAAAQAAR0Ehc3RhY2tfcmVhbGlnbgAAAAAKAAAAEAAAAAAB +AABHQSQBM2M4NzEAAADADwAAAAAAAMAPAAAAAAAAFwAAAAAAAAAAAQAAR0EkBWdjYyA5LjIuMSAy +MDE5MDgyNwAACgAAAAAAAAAAAQAAR0EqR09XAKpEAAAABgAAAAAAAAAAAQAAR0EqAgEAAAAPAAAA +AAAAAAABAABHQStzdGFja19jbGFzaAAAEwAAAAAAAAAAAQAAR0EqY2ZfcHJvdGVjdGlvbgACAAAN +AAAAAAAAAAABAABHQSpGT1JUSUZZAAIAAAAAFgAAAAAAAAAAAQAAR0ErR0xJQkNYWF9BU1NFUlRJ +T05TAAAABgAAAAAAAAAAAQAAR0EqBwIAAAAFAAAAAAAAAAABAABHQSEIAAAAABYAAAAAAAAAAAEA +AEdBIW9taXRfZnJhbWVfcG9pbnRlcgAAAAwAAAAAAAAAAAEAAEdBKgYSAAAAEQAMABEAAAAAAAAA +AAEAAEdBIXN0YWNrX3JlYWxpZ24AAAAACgAAABAAAAAAAQAAR0EkATNzODcxAAAAwA8AAAAAAAAQ +FAAAAAAAABcAAAAAAAAAAAEAAEdBJAVnY2MgOS4yLjEgMjAxOTA4MjcAAAoAAAAAAAAAAAEAAEdB +KkdPVwCqRAAAAAYAAAAAAAAAAAEAAEdBKgIBAAAADwAAAAAAAAAAAQAAR0Erc3RhY2tfY2xhc2gA +ABMAAAAAAAAAAAEAAEdBKmNmX3Byb3RlY3Rpb24AAgAADQAAAAAAAAAAAQAAR0EqRk9SVElGWQAC +AAAAABYAAAAAAAAAAAEAAEdBK0dMSUJDWFhfQVNTRVJUSU9OUwAAAAYAAAAAAAAAAAEAAEdBKgcC +AAAABQAAAAAAAAAAAQAAR0EhCAAAAAAWAAAAAAAAAAABAABHQSFvbWl0X2ZyYW1lX3BvaW50ZXIA +AAAMAAAAAAAAAAABAABHQSoGEgAAABEADAARAAAAAAAAAAABAABHQSFzdGFja19yZWFsaWduAAAA +AAoAAAAQAAAAAAEAAEdBJAEzZTg3MQAAAMAPAAAAAAAAwA8AAAAAAAAXAAAAAAAAAAABAABHQSQF +Z2NjIDkuMi4xIDIwMTkwODI3AAAKAAAAAAAAAAABAABHQSpHT1cAqkQAAAAGAAAAAAAAAAABAABH +QSoCAQAAAA8AAAAAAAAAAAEAAEdBK3N0YWNrX2NsYXNoAAATAAAAAAAAAAABAABHQSpjZl9wcm90 +ZWN0aW9uAAIAAA0AAAAAAAAAAAEAAEdBKkZPUlRJRlkAAgAAAAAWAAAAAAAAAAABAABHQStHTElC +Q1hYX0FTU0VSVElPTlMAAAAGAAAAAAAAAAABAABHQSoHAgAAAAUAAAAAAAAAAAEAAEdBIQgAAAAA +FgAAAAAAAAAAAQAAR0Ehb21pdF9mcmFtZV9wb2ludGVyAAAADAAAAAAAAAAAAQAAR0EqBhIAAAAR +AAwAEQAAAAAAAAAAAQAAR0Ehc3RhY2tfcmVhbGlnbgAAAAAMAAAAEAAAAAEBAABHQSoGEgAAABEA +DAD5FAAAAAAAAPkUAAAAAAAAEQAAAAAAAAABAQAAR0Ehc3RhY2tfcmVhbGlnbgAAAAAGAAAAAAAA +AAEBAABHQSoCAQAAAA8AAAAAAAAAAQEAAEdBK3N0YWNrX2NsYXNoAAATAAAAAAAAAAEBAABHQSpj +Zl9wcm90ZWN0aW9uAAIAABYAAAAAAAAAAQEAAEdBIW9taXRfZnJhbWVfcG9pbnRlcgAAAAYAAAAA +AAAAAQEAAEdBKgcCAAAACgAAAAAAAAABAQAAR0EqR09XAKpEAAAABQAAAAAAAAABAQAAR0EhCAAA +AAANAAAAAAAAAAEBAABHQSpGT1JUSUZZAAIAAAAAFgAAAAAAAAABAQAAR0ErR0xJQkNYWF9BU1NF +UlRJT05TAAAACAAAABAAAAAAAQAAR0EkATNhMQAAFQAAAAAAAHUVAAAAAAAACAAAABAAAAAAAQAA +R0EkATNhMQB1FQAAAAAAAHUVAAAAAAAACAAAABAAAAAAAQAAR0EkATNhMQB1FQAAAAAAAHUVAAAA +AAAACAAAABAAAAAAAQAAR0EkATNhMQBmDQAAAAAAAGsNAAAAAAAACAAAABAAAAAAAQAAR0EkATNh +MQCAFQAAAAAAAIUVAAAAAAAABAAAAAkAAAAEAAAAR05VAGdvbGQgMS4xNgAAAGR1bXBtc2NhdC00 +LjEwLjgtMC5mYzMwLng4Nl82NC5kZWJ1ZwAAAAC4mTD+/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj +4A+fA1BdAD+RRYRoPYmm2orhgzJO2QO/MNCOsUDbyeYDgZGDCQaOME3ZRlihjM8aDuLulF/zKdD6 +buwnAvzLui/mrNXqjiNg3AFyJghiUKnLwABb6HXwaWFA14laZtLTAzRhRge183B3SALfT4C4So19 +73sabpi1xH5o7rP0H95099NKTy1/TlkIOvubvF5m6kPshxWteEvuvwZk96RdTJULkif5npLiqBNO +LckWuNsvJTLePc97p/Lt7AL9ZPgKlLOzC3tQtgmfTy+jS+VceTqkdZ0dYtuo74pY2wrUB3xOzJ1+ +5BAULCnpNvapaCQYA47ExNg6UbQmzkJK+VPbvKG+xw5tusGEzKLriTPb0NgdaG/D+M7znrLrnvot +FJX+tgbwLswuwjTlClr67cMn6KNTgGV8Jg5A7QSMt7OqZf7ksdS2QBz8+D+pQBB0VSdlgINigccU +TMbJFUFs/UMqOvDQFvCrhZxdziFjgWi7V+PkbboYqhULA4/7+pd5DhVze8vmtSmrDOG4asN7edc4 +IYsGMjSFlkXvTHt30vo8niAFzTcx2NbqS0I6A9x29Ynwa9AoJmsy7CU80+UVBkat1xG/bR+NEErr +eUiAEnR3h80HLiWIG/fHQILt4Y1a8y9uyjVFQpb1/JEY2+A1e2iO1iR7eAGR4gQvUuSCUDOgzwkb +7eSTLZc5LIBJSy+dtVON8msbXxuPJoZhOfCUcK49s5uaNw/q7lmnfrEclyLMVPK04LcGbexa2lHu +QDx7FCp35AiEUQYFbPbjeQWPi6jXakcrcI/HiICJmjGAlK72RlRX1/MSJWYAjghtPMMGxTc1TiOU +QkmXNs9RIPYwvdoy+Y8CCjDXJg0lRtZHzUR4KUgFTC6Tqd8Bdpp5LRKp03sTj78O4fkNXUwzrIv/ +S4+7KXnkLhEN5A8uaseY0R4Po2Og1mL9l7ddhnUo71AIiLBitlTfwWtbLAi3Vcnz+wegZah+2GZs +ZosCRwrXkom367ONXZgxqZcFfAjFezTvcloPOTBP2LJaIgPcUr3MJI8d4SPIR7ytrBdtGz5ZDd65 +QYyH1Pj70XMyXQRvVLxCxSc63CeLWssGy/phoWd1+tb/G886PGo/rFRs6SoFVdnUtbO54poHDmoA +AFKJF9LL7J3aAAHsBqAfAADMB4J3scRn+wIAAAAABFlaAC5zaHN0cnRhYgAuaW50ZXJwAC5ub3Rl +LkFCSS10YWcALm5vdGUuZ251LnByb3BlcnR5AC5ub3RlLmdudS5idWlsZC1pZAAuZHluc3ltAC5k +eW5zdHIALmdudS5oYXNoAC5nbnUudmVyc2lvbgAuZ251LnZlcnNpb25fcgAucmVsYS5keW4ALnJl +bGEucGx0AC5pbml0AC50ZXh0AC5maW5pAC5yb2RhdGEALmVoX2ZyYW1lAC5laF9mcmFtZV9oZHIA +LmRhdGEucmVsLnJvLmxvY2FsAC5maW5pX2FycmF5AC5pbml0X2FycmF5AC5keW5hbWljAC5nb3QA +LmdvdC5wbHQALmRhdGEALnRtX2Nsb25lX3RhYmxlAC5ic3MALmdudS5idWlsZC5hdHRyaWJ1dGVz +AC5ub3RlLmdudS5nb2xkLXZlcnNpb24ALmdudV9kZWJ1Z2xpbmsALmdudV9kZWJ1Z2RhdGEAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAACwAAAAEAAAACAAAAAAAAADgCAAAAAAAAOAIAAAAAAAAcAAAAAAAAAAAAAAAAAAAA +AQAAAAAAAAAAAAAAAAAAABMAAAAHAAAAAgAAAAAAAABUAgAAAAAAAFQCAAAAAAAAIAAAAAAAAAAA +AAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAhAAAABwAAAAIAAAAAAAAAdAIAAAAAAAB0AgAAAAAAACAA +AAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAANAAAAAcAAAACAAAAAAAAAJQCAAAAAAAAlAIA +AAAAAAAkAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAEcAAAALAAAAAgAAAAAAAAC4AgAA +AAAAALgCAAAAAAAAMAMAAAAAAAAGAAAAAQAAAAgAAAAAAAAAGAAAAAAAAABPAAAAAwAAAAIAAAAA +AAAA6AUAAAAAAADoBQAAAAAAAMkDAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAVwAAAPb/ +/28CAAAAAAAAALgJAAAAAAAAuAkAAAAAAABMAAAAAAAAAAUAAAAAAAAACAAAAAAAAAAAAAAAAAAA +AGEAAAD///9vAgAAAAAAAAAECgAAAAAAAAQKAAAAAAAARAAAAAAAAAAFAAAAAAAAAAIAAAAAAAAA +AgAAAAAAAABuAAAA/v//bwIAAAAAAAAASAoAAAAAAABICgAAAAAAAIAAAAAAAAAABgAAAAMAAAAE +AAAAAAAAAAAAAAAAAAAAfQAAAAQAAAACAAAAAAAAAMgKAAAAAAAAyAoAAAAAAADYAAAAAAAAAAUA +AAAAAAAACAAAAAAAAAAYAAAAAAAAAIcAAAAEAAAAQgAAAAAAAACgCwAAAAAAAKALAAAAAAAAsAEA +AAAAAAAFAAAAGAAAAAgAAAAAAAAAGAAAAAAAAACRAAAAAQAAAAYAAAAAAAAAUA0AAAAAAABQDQAA +AAAAABsAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAjAAAAAEAAAAGAAAAAAAAAHANAAAA +AAAAcA0AAAAAAABQAgAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAJcAAAABAAAABgAAAAAA +AADADwAAAAAAAMAPAAAAAAAAtQUAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAACdAAAAAQAA +AAYAAAAAAAAAeBUAAAAAAAB4FQAAAAAAAA0AAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA +owAAAAEAAAACAAAAAAAAAIgVAAAAAAAAiBUAAAAAAAC0AQAAAAAAAAAAAAAAAAAACAAAAAAAAAAA +AAAAAAAAAKsAAAABAABwAgAAAAAAAABAFwAAAAAAAEAXAAAAAAAADAEAAAAAAAAAAAAAAAAAAAgA +AAAAAAAAAAAAAAAAAAC1AAAAAQAAcAIAAAAAAAAATBgAAAAAAABMGAAAAAAAADQAAAAAAAAAAAAA +AAAAAAAEAAAAAAAAAAAAAAAAAAAAwwAAAAEAAAADAAAAAAAAALArAAAAAAAAsBsAAAAAAAAIAAAA +AAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAANYAAAAPAAAAAwAAAAAAAAC4KwAAAAAAALgbAAAA +AAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAADiAAAADgAAAAMAAAAAAAAAwCsAAAAA +AADAGwAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA7gAAAAYAAAADAAAAAAAA +AMgrAAAAAAAAyBsAAAAAAABgAwAAAAAAAAYAAAAAAAAACAAAAAAAAAAQAAAAAAAAAPcAAAABAAAA +AwAAAAAAAAAoLwAAAAAAACgfAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAD8 +AAAAAQAAAAMAAAAAAAAAWC8AAAAAAABYHwAAAAAAAKgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAA +AAAAAAAABQEAAAEAAAADAAAAAAAAAAAwAAAAAAAAACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAQAA +AAAAAAAAAAAAAAAAAAsBAAABAAAAAwAAAAAAAAAIMAAAAAAAAAggAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAgAAAAAAAAAAAAAAAAAAAAbAQAACAAAAAMAAAAAAAAACDAAAAAAAAAIIAAAAAAAAAEAAAAA +AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAIAEAAAcAAAAAAAAAAAAAAAAAAAAAAAAACCAAAAAA +AAB4EQAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAADYBAAAHAAAAAAAAAAAAAAAAAAAAAAAA +AIAxAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAABNAQAAAQAAAAAAAAAAAAAA +AAAAAAAAAACcMQAAAAAAACwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAXAEAAAEAAAAA +AAAAAAAAAAAAAAAAAAAAyDEAAAAAAACQAwAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEA +AAADAAAAAAAAAAAAAAAAAAAAAAAAAFg1AAAAAAAAawEAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAA +AAAAAAA=` + +// A binary containing nothing but a main function. It is not compiled as PIE. +// The symbol 'main' is at 0x401106 and the symbol '_start' is at 0x401020. +// It also has no build ID (-Wl,--build-id=none) or debug link. +var helloworldBin = `f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAIBBAAAAAAABAAAAAAAAAAGhAAAAAAAAAAAAAAEAAOAAL +AEAAGwAaAAYAAAAEAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAaAIAAAAAAABoAgAAAAAAAAgA +AAAAAAAAAwAAAAQAAACoAgAAAAAAAKgCQAAAAAAAqAJAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA +AAAAAAABAAAABAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAOADAAAAAAAA4AMAAAAAAAAAEAAA +AAAAAAEAAAAFAAAAABAAAAAAAAAAEEAAAAAAAAAQQAAAAAAApQEAAAAAAAClAQAAAAAAAAAQAAAA +AAAAAQAAAAQAAAAAIAAAAAAAAAAgQAAAAAAAACBAAAAAAAAIAQAAAAAAAAgBAAAAAAAAABAAAAAA +AAABAAAABgAAAFAuAAAAAAAAUD5AAAAAAABQPkAAAAAAAMwBAAAAAAAA0AEAAAAAAAAAEAAAAAAA +AAIAAAAGAAAAYC4AAAAAAABgPkAAAAAAAGA+QAAAAAAAkAEAAAAAAACQAQAAAAAAAAgAAAAAAAAA +BAAAAAQAAADEAgAAAAAAAMQCQAAAAAAAxAJAAAAAAAAgAAAAAAAAACAAAAAAAAAABAAAAAAAAABQ +5XRkBAAAABAgAAAAAAAAECBAAAAAAAAQIEAAAAAAADQAAAAAAAAANAAAAAAAAAAEAAAAAAAAAFHl +dGQGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAUuV0 +ZAQAAABQLgAAAAAAAFA+QAAAAAAAUD5AAAAAAACwAQAAAAAAALABAAAAAAAAAQAAAAAAAAAvbGli +NjQvbGQtbGludXgteDg2LTY0LnNvLjIABAAAABAAAAABAAAAR05VAAAAAAADAAAAAgAAAAAAAAAA +AAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAALAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAApAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAbGliYy5z +by42AF9fbGliY19zdGFydF9tYWluAEdMSUJDXzIuMi41AF9fZ21vbl9zdGFydF9fAAAAAgAAAAAA +AQABAAEAAAAQAAAAAAAAAHUaaQkAAAIAHQAAAAAAAADwP0AAAAAAAAYAAAABAAAAAAAAAAAAAAD4 +P0AAAAAAAAYAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPMPHvpIg+wI +SIsF6S8AAEiFwHQC/9BIg8QIwwAAAAAA8w8e+jHtSYnRXkiJ4kiD5PBQVEnHwJARQABIx8EgEUAA +SMfHBhFAAP8Voi8AAPSQ8w8e+sNmLg8fhAAAAAAAkLggQEAASD0gQEAAdBO4AAAAAEiFwHQJvyBA +QAD/4GaQw2ZmLg8fhAAAAAAADx9AAL4gQEAASIHuIEBAAEiJ8EjB7j9IwfgDSAHGSNH+dBG4AAAA +AEiFwHQHvyBAQAD/4MNmZi4PH4QAAAAAAA8fQADzDx76gD1BLwAAAHUTVUiJ5eh6////xgUvLwAA +AV3DkMNmZi4PH4QAAAAAAA8fQADzDx7664pVSInluAAAAABdw2YuDx+EAAAAAAAPH0QAAPMPHvpB +V0yNPSMtAABBVkmJ1kFVSYn1QVRBifxVSI0tFC0AAFNMKf1Ig+wI6K/+//9Iwf0DdB8x2w8fgAAA +AABMifJMie5EiedB/xTfSIPDAUg53XXqSIPECFtdQVxBXUFeQV/DZmYuDx+EAAAAAADzDx76wwAA +APMPHvpIg+wISIPECMMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIAAAAAAAAAAAAAAAAA +ARsDOzQAAAAFAAAAEPD//1AAAABA8P//ZAAAAPbw//94AAAAEPH//5gAAACA8f//4AAAAAAAAAAU +AAAAAAAAAAF6UgABeBABGwwHCJABAAAQAAAAHAAAALjv//8vAAAAAEQHEBAAAAAwAAAA1O///wUA +AAAAAAAAHAAAAEQAAAB28P//CwAAAABBDhCGAkMNBkYMBwgAAABEAAAAZAAAAHDw//9lAAAAAEYO +EI8CSQ4YjgNFDiCNBEUOKIwFRA4whgZIDjiDB0cOQG4OOEEOMEEOKEIOIEIOGEIOEEIOCAAQAAAA +rAAAAJjw//8FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +ABFAAAAAAADQEEAAAAAAAAEAAAAAAAAAAQAAAAAAAAAMAAAAAAAAAAAQQAAAAAAADQAAAAAAAACY +EUAAAAAAABkAAAAAAAAAUD5AAAAAAAAbAAAAAAAAAAgAAAAAAAAAGgAAAAAAAABYPkAAAAAAABwA +AAAAAAAACAAAAAAAAAD1/v9vAAAAAOgCQAAAAAAABQAAAAAAAABQA0AAAAAAAAYAAAAAAAAACANA +AAAAAAAKAAAAAAAAADgAAAAAAAAACwAAAAAAAAAYAAAAAAAAABUAAAAAAAAAAAAAAAAAAAAHAAAA +AAAAALADQAAAAAAACAAAAAAAAAAwAAAAAAAAAAkAAAAAAAAAGAAAAAAAAAD+//9vAAAAAJADQAAA +AAAA////bwAAAAABAAAAAAAAAPD//28AAAAAiANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYD5AAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAEdDQzogKEdOVSkgOS4yLjEgMjAxOTA4MjcgKFJlZCBIYXQgOS4yLjEtMSkADAAAABAAAAAB +AQAAR0EkATNoODcxAAAAIBBAAAAAAAAgEEAAAAAAAAgAAAAQAAAAAQEAAEdBJAEzYTEAIBBAAAAA +AABPEEAAAAAAAAwAAAAQAAAAAQEAAEdBJAEzcDg3MQAAAFAQQAAAAAAAVRBAAAAAAAAYAAAAAAAA +AAABAABHQSQFZ2NjIDkuMi4xIDIwMTkwODI3AAAMAAAAAAAAAAEBAABHQSpHT1cAqkQAAAAIAAAA +AAAAAAABAABHQSoCAQAAABAAAAAAAAAAAQEAAEdBK3N0YWNrX2NsYXNoAAAUAAAAAAAAAAABAABH +QSpjZl9wcm90ZWN0aW9uAAIAABAAAAAAAAAAAAEAAEdBKkZPUlRJRlkA/wAAAAAYAAAAAAAAAAEB +AABHQStHTElCQ1hYX0FTU0VSVElPTlMAAAAIAAAAAAAAAAABAABHQSoHAwAAAAgAAAAAAAAAAAEA +AEdBIQgAAAAAGAAAAAAAAAAAAQAAR0Ehb21pdF9mcmFtZV9wb2ludGVyAAAADAAAAAAAAAAAAQAA +R0EqBhIAAAARAAwAFAAAAAAAAAABAQAAR0Ehc3RhY2tfcmVhbGlnbgAAAAAMAAAAAAAAAAEBAABH +QSpHT1cAqkQAAAAQAAAAAAAAAAEBAABHQStzdGFja19jbGFzaAAAGAAAAAAAAAABAQAAR0ErR0xJ +QkNYWF9BU1NFUlRJT05TAAAAFAAAAAAAAAABAQAAR0Ehc3RhY2tfcmVhbGlnbgAAAAAMAAAAAAAA +AAEBAABHQSpHT1cAqkQAAAAQAAAAAAAAAAEBAABHQStzdGFja19jbGFzaAAAGAAAAAAAAAABAQAA +R0ErR0xJQkNYWF9BU1NFUlRJT05TAAAAFAAAAAAAAAABAQAAR0Ehc3RhY2tfcmVhbGlnbgAAAAAM +AAAAAAAAAAEBAABHQSpHT1cAqkQAAAAQAAAAAAAAAAEBAABHQStzdGFja19jbGFzaAAAGAAAAAAA +AAABAQAAR0ErR0xJQkNYWF9BU1NFUlRJT05TAAAAFAAAAAAAAAABAQAAR0Ehc3RhY2tfcmVhbGln +bgAAAAAMAAAAAAAAAAEBAABHQSpHT1cAqkQAAAAQAAAAAAAAAAEBAABHQStzdGFja19jbGFzaAAA +GAAAAAAAAAABAQAAR0ErR0xJQkNYWF9BU1NFUlRJT05TAAAAFAAAAAAAAAABAQAAR0Ehc3RhY2tf +cmVhbGlnbgAAAAAIAAAAEAAAAAEBAABHQSQBM2ExAAAQQAAAAAAAFhBAAAAAAAAIAAAAEAAAAAAB +AABHQSQBM2ExAJgRQAAAAAAApRFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA +AAACCgBPEEAAAAAAAAAAAAAAAAAAEQAAAAACCgBPEEAAAAAAAAAAAAAAAAAAJQAAAAACCgAgEEAA +AAAAAAAAAAAAAAAAOQAAAAACCgAgEEAAAAAAAAAAAAAAAAAAUQAAAAACCgAgEEAAAAAAAAAAAAAA +AAAAagAAAAACCgAgEEAAAAAAAAAAAAAAAAAAhwAAAAACCgAgEEAAAAAAAAAAAAAAAAAAnwAAAAAC +CgAgEEAAAAAAAAAAAAAAAAAAuwAAAAACCgAgEEAAAAAAAAAAAAAAAAAA0AAAAAACCgAgEEAAAAAA +AAAAAAAAAAAA6QAAAAACCgBQEEAAAAAAAAAAAAAAAAAAAQEAAAACCgBVEEAAAAAAAAAAAAAAAAAA +HQEAAAACCgAgEEAAAAAAAAAAAAAAAAAAOQEAAAACCgAgEEAAAAAAAAAAAAAAAAAAWQEAAAACCgAg +EEAAAAAAAAAAAAAAAAAAegEAAAACCgAgEEAAAAAAAAAAAAAAAAAAnwEAAAACCgAgEEAAAAAAAAAA +AAAAAAAAvwEAAAACCgAgEEAAAAAAAAAAAAAAAAAA4wEAAAACCgAgEEAAAAAAAAAAAAAAAAAAAAIA +AAACCgAgEEAAAAAAAAAAAAAAAAAAIQIAAAACCgBVEEAAAAAAAAAAAAAAAAAASAIAAAACCgBVEEAA +AAAAAAAAAAAAAAAAbQIAAAIACgBgEEAAAAAAAAAAAAAAAAAAbwIAAAIACgCQEEAAAAAAAAAAAAAA +AAAAggIAAAIACgDQEEAAAAAAAAAAAAAAAAAAmAIAAAEAFQAcQEAAAAAAAAEAAAAAAAAApwIAAAEA +EABYPkAAAAAAAAAAAAAAAAAAzgIAAAIACgAAEUAAAAAAAAAAAAAAAAAA2gIAAAEADwBQPkAAAAAA +AAAAAAAAAAAA+QIAAAEADgAEIUAAAAAAAAAAAAAAAAAABwMAAAAADwBYPkAAAAAAAAAAAAAAAAAA +GAMAAAEAEQBgPkAAAAAAAAAAAAAAAAAAIQMAAAAADwBQPkAAAAAAAAAAAAAAAAAANAMAAAAADQAQ +IEAAAAAAAAAAAAAAAAAARwMAAAEAEwAAQEAAAAAAAAAAAAAAAAAAAAAAAAMAAQCoAkAAAAAAAAAA +AAAAAAAAAAAAAAMAAgDEAkAAAAAAAAAAAAAAAAAAAAAAAAMAAwDoAkAAAAAAAAAAAAAAAAAAAAAA +AAMABAAIA0AAAAAAAAAAAAAAAAAAAAAAAAMABQBQA0AAAAAAAAAAAAAAAAAAAAAAAAMABgCIA0AA +AAAAAAAAAAAAAAAAAAAAAAMABwCQA0AAAAAAAAAAAAAAAAAAAAAAAAMACACwA0AAAAAAAAAAAAAA +AAAAAAAAAAMACQAAEEAAAAAAAAAAAAAAAAAAAAAAAAMACgAgEEAAAAAAAAAAAAAAAAAAAAAAAAMA +CwCYEUAAAAAAAAAAAAAAAAAAAAAAAAMADAAAIEAAAAAAAAAAAAAAAAAAAAAAAAMADQAQIEAAAAAA +AAAAAAAAAAAAAAAAAAMADgBIIEAAAAAAAAAAAAAAAAAAAAAAAAMADwBQPkAAAAAAAAAAAAAAAAAA +AAAAAAMAEABYPkAAAAAAAAAAAAAAAAAAAAAAAAMAEQBgPkAAAAAAAAAAAAAAAAAAAAAAAAMAEgDw +P0AAAAAAAAAAAAAAAAAAAAAAAAMAEwAAQEAAAAAAAAAAAAAAAAAAAAAAAAMAFAAYQEAAAAAAAAAA +AAAAAAAAAAAAAAMAFQAcQEAAAAAAAAAAAAAAAAAAAAAAAAMAFgAAAAAAAAAAAAAAAAAAAAAAAAAA +AAMAFwAgYEAAAAAAAAAAAAAAAAAAXQMAABIACgCQEUAAAAAAAAUAAAAAAAAAlQMAACAAFAAYQEAA +AAAAAAAAAAAAAAAAbQMAABAAFAAcQEAAAAAAAAAAAAAAAAAAZwMAABICCwCYEUAAAAAAAAAAAAAA +AAAAdAMAABIAAAAAAAAAAAAAAAAAAAAAAAAAkwMAABAAFAAYQEAAAAAAAAAAAAAAAAAAoAMAACAA +AAAAAAAAAAAAAAAAAAAAAAAArwMAABECDAAIIEAAAAAAAAAAAAAAAAAAvAMAABEADAAAIEAAAAAA +AAQAAAAAAAAAywMAABIACgAgEUAAAAAAAGUAAAAAAAAAGAEAABAAFQAgQEAAAAAAAAAAAAAAAAAA +2wMAABICCgBQEEAAAAAAAAUAAAAAAAAAmQMAABIACgAgEEAAAAAAAC8AAAAAAAAA8wMAABAAFQAc +QEAAAAAAAAAAAAAAAAAA/wMAABIACgAGEUAAAAAAAAsAAAAAAAAABAQAABECFAAgQEAAAAAAAAAA +AAAAAAAA1QMAABICCQAAEEAAAAAAAAAAAAAAAAAAAC5hbm5vYmluX2luaXQuYwAuYW5ub2Jpbl9p +bml0LmNfZW5kAC5hbm5vYmluX2luaXQuYy5ob3QALmFubm9iaW5faW5pdC5jX2VuZC5ob3QALmFu +bm9iaW5faW5pdC5jLnVubGlrZWx5AC5hbm5vYmluX2luaXQuY19lbmQudW5saWtlbHkALmFubm9i +aW5faW5pdC5jLnN0YXJ0dXAALmFubm9iaW5faW5pdC5jX2VuZC5zdGFydHVwAC5hbm5vYmluX2lu +aXQuYy5leGl0AC5hbm5vYmluX2luaXQuY19lbmQuZXhpdAAuYW5ub2Jpbl9zdGF0aWNfcmVsb2Mu +YwAuYW5ub2Jpbl9zdGF0aWNfcmVsb2MuY19lbmQALmFubm9iaW5fc3RhdGljX3JlbG9jLmMuaG90 +AC5hbm5vYmluX3N0YXRpY19yZWxvYy5jX2VuZC5ob3QALmFubm9iaW5fc3RhdGljX3JlbG9jLmMu +dW5saWtlbHkALmFubm9iaW5fc3RhdGljX3JlbG9jLmNfZW5kLnVubGlrZWx5AC5hbm5vYmluX3N0 +YXRpY19yZWxvYy5jLnN0YXJ0dXAALmFubm9iaW5fc3RhdGljX3JlbG9jLmNfZW5kLnN0YXJ0dXAA +LmFubm9iaW5fc3RhdGljX3JlbG9jLmMuZXhpdAAuYW5ub2Jpbl9zdGF0aWNfcmVsb2MuY19lbmQu +ZXhpdAAuYW5ub2Jpbl9fZGxfcmVsb2NhdGVfc3RhdGljX3BpZS5zdGFydAAuYW5ub2Jpbl9fZGxf +cmVsb2NhdGVfc3RhdGljX3BpZS5lbmQAZGVyZWdpc3Rlcl90bV9jbG9uZXMAX19kb19nbG9iYWxf +ZHRvcnNfYXV4AGNvbXBsZXRlZC43Mzg3AF9fZG9fZ2xvYmFsX2R0b3JzX2F1eF9maW5pX2FycmF5 +X2VudHJ5AGZyYW1lX2R1bW15AF9fZnJhbWVfZHVtbXlfaW5pdF9hcnJheV9lbnRyeQBfX0ZSQU1F +X0VORF9fAF9faW5pdF9hcnJheV9lbmQAX0RZTkFNSUMAX19pbml0X2FycmF5X3N0YXJ0AF9fR05V +X0VIX0ZSQU1FX0hEUgBfR0xPQkFMX09GRlNFVF9UQUJMRV8AX19saWJjX2NzdV9maW5pAF9lZGF0 +YQBfX2xpYmNfc3RhcnRfbWFpbkBAR0xJQkNfMi4yLjUAX19kYXRhX3N0YXJ0AF9fZ21vbl9zdGFy +dF9fAF9fZHNvX2hhbmRsZQBfSU9fc3RkaW5fdXNlZABfX2xpYmNfY3N1X2luaXQAX2RsX3JlbG9j +YXRlX3N0YXRpY19waWUAX19ic3Nfc3RhcnQAbWFpbgBfX1RNQ19FTkRfXwAALnN5bXRhYgAuc3Ry +dGFiAC5zaHN0cnRhYgAuaW50ZXJwAC5ub3RlLkFCSS10YWcALmdudS5oYXNoAC5keW5zeW0ALmR5 +bnN0cgAuZ251LnZlcnNpb24ALmdudS52ZXJzaW9uX3IALnJlbGEuZHluAC5pbml0AC50ZXh0AC5m +aW5pAC5yb2RhdGEALmVoX2ZyYW1lX2hkcgAuZWhfZnJhbWUALmluaXRfYXJyYXkALmZpbmlfYXJy +YXkALmR5bmFtaWMALmdvdAAuZ290LnBsdAAuZGF0YQAuYnNzAC5jb21tZW50AC5nbnUuYnVpbGQu +YXR0cmlidXRlcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAABsAAAABAAAAAgAAAAAAAACoAkAAAAAAAKgCAAAAAAAAHAAA +AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAjAAAABwAAAAIAAAAAAAAAxAJAAAAAAADEAgAA +AAAAACAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAMQAAAPb//28CAAAAAAAAAOgCQAAA +AAAA6AIAAAAAAAAcAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAADsAAAALAAAAAgAAAAAA +AAAIA0AAAAAAAAgDAAAAAAAASAAAAAAAAAAFAAAAAQAAAAgAAAAAAAAAGAAAAAAAAABDAAAAAwAA +AAIAAAAAAAAAUANAAAAAAABQAwAAAAAAADgAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA +SwAAAP///28CAAAAAAAAAIgDQAAAAAAAiAMAAAAAAAAGAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAC +AAAAAAAAAFgAAAD+//9vAgAAAAAAAACQA0AAAAAAAJADAAAAAAAAIAAAAAAAAAAFAAAAAQAAAAgA +AAAAAAAAAAAAAAAAAABnAAAABAAAAAIAAAAAAAAAsANAAAAAAACwAwAAAAAAADAAAAAAAAAABAAA +AAAAAAAIAAAAAAAAABgAAAAAAAAAcQAAAAEAAAAGAAAAAAAAAAAQQAAAAAAAABAAAAAAAAAbAAAA +AAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAHcAAAABAAAABgAAAAAAAAAgEEAAAAAAACAQAAAA +AAAAdQEAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAB9AAAAAQAAAAYAAAAAAAAAmBFAAAAA +AACYEQAAAAAAAA0AAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAgwAAAAEAAAACAAAAAAAA +AAAgQAAAAAAAACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAIsAAAABAAAA +AgAAAAAAAAAQIEAAAAAAABAgAAAAAAAANAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAACZ +AAAAAQAAAAIAAAAAAAAASCBAAAAAAABIIAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAA +AAAAAAAAowAAAA4AAAADAAAAAAAAAFA+QAAAAAAAUC4AAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAA +AAAAAAAIAAAAAAAAAK8AAAAPAAAAAwAAAAAAAABYPkAAAAAAAFguAAAAAAAACAAAAAAAAAAAAAAA +AAAAAAgAAAAAAAAACAAAAAAAAAC7AAAABgAAAAMAAAAAAAAAYD5AAAAAAABgLgAAAAAAAJABAAAA +AAAABQAAAAAAAAAIAAAAAAAAABAAAAAAAAAAxAAAAAEAAAADAAAAAAAAAPA/QAAAAAAA8C8AAAAA +AAAQAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAMkAAAABAAAAAwAAAAAAAAAAQEAAAAAA +AAAwAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAADSAAAAAQAAAAMAAAAAAAAA +GEBAAAAAAAAYMAAAAAAAAAQAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA2AAAAAgAAAAD +AAAAAAAAABxAQAAAAAAAHDAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAN0A +AAABAAAAMAAAAAAAAAAAAAAAAAAAABwwAAAAAAAALAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAA +AAAAAADmAAAABwAAAAAAAAAAAAAAIGBAAAAAAABIMAAAAAAAAOwDAAAAAAAAAAAAAAAAAAAEAAAA +AAAAAAAAAAAAAAAAAQAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAODQAAAAAAAAgBwAAAAAAABkAAAA7 +AAAACAAAAAAAAAAYAAAAAAAAAAkAAAADAAAAAAAAAAAAAAAAAAAAAAAAAFg7AAAAAAAAEAQAAAAA +AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAARAAAAAwAAAAAAAAAAAAAAAAAAAAAAAABoPwAAAAAA +APwAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA` + +// A shared library with a function called 'func' at offset 0x1000 +var sharedlib = `f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAABAAAAAAAABAAAAAAAAAANgxAAAAAAAAAAAAAEAAOAAH +AEAADAALAAEAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATQIAAAAAAABNAgAAAAAAAAAQ +AAAAAAAAAQAAAAUAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAALAAAAAAAAAAsAAAAAAAAAABAA +AAAAAAABAAAABAAAAAAgAAAAAAAAACAAAAAAAAAAIAAAAAAAADgAAAAAAAAAOAAAAAAAAAAAEAAA +AAAAAAEAAAAGAAAAMC8AAAAAAAAwPwAAAAAAADA/AAAAAAAA0AAAAAAAAADQAAAAAAAAAAAQAAAA +AAAAAgAAAAYAAAAwLwAAAAAAADA/AAAAAAAAMD8AAAAAAADQAAAAAAAAANAAAAAAAAAACAAAAAAA +AABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAA +AFLldGQEAAAAMC8AAAAAAAAwPwAAAAAAADA/AAAAAAAA0AAAAAAAAADQAAAAAAAAAAEAAAAAAAAA +AQAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAEAAAABAAAABgAAAAAAAAAAAAICAAAAAAEAAABx +/pZ8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAASAAUAABAAAAAAAAALAAAAAAAAAABm +dW5jAGxpYm9wdGl0ZXN0LnNvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVIieW4AAAA +AF3DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAF6UgABeBAB +GwwHCJABAAAcAAAAHAAAAODv//8LAAAAAEEOEIYCQw0GRgwHCAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAA +AAAAAAYAAAAAAAAABAAAAAAAAADIAQAAAAAAAPX+/28AAAAA4AEAAAAAAAAFAAAAAAAAADgCAAAA +AAAABgAAAAAAAAAIAgAAAAAAAAoAAAAAAAAAFQAAAAAAAAALAAAAAAAAABgAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR0NDOiAoR05VKSA5LjIuMSAyMDE5MDgy +NyAoUmVkIEhhdCA5LjIuMS0xKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAQDI +AQAAAAAAAAAAAAAAAAAAAAAAAAMAAgDgAQAAAAAAAAAAAAAAAAAAAAAAAAMAAwAIAgAAAAAAAAAA +AAAAAAAAAAAAAAMABAA4AgAAAAAAAAAAAAAAAAAAAAAAAAMABQAAEAAAAAAAAAAAAAAAAAAAAAAA +AAMABgAAIAAAAAAAAAAAAAAAAAAAAAAAAAMABwAwPwAAAAAAAAAAAAAAAAAAAAAAAAMACAAAAAAA +AAAAAAAAAAAAAAAAAQAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAAAAAAAQA8f8AAAAAAAAAAAAAAAAA +AAAABwAAAAEABwAwPwAAAAAAAAAAAAAAAAAAEAAAABIABQAAEAAAAAAAAAsAAAAAAAAAAGxpYi5j +AF9EWU5BTUlDAGZ1bmMAAC5zeW10YWIALnN0cnRhYgAuc2hzdHJ0YWIALmdudS5oYXNoAC5keW5z +eW0ALmR5bnN0cgAudGV4dAAuZWhfZnJhbWUALmR5bmFtaWMALmNvbW1lbnQAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf +AAAABQAAAAIAAAAAAAAAyAEAAAAAAADIAQAAAAAAABQAAAAAAAAAAwAAAAAAAAAIAAAAAAAAAAQA +AAAAAAAAGwAAAPb//28CAAAAAAAAAOABAAAAAAAA4AEAAAAAAAAkAAAAAAAAAAMAAAAAAAAACAAA +AAAAAAAAAAAAAAAAACUAAAALAAAAAgAAAAAAAAAIAgAAAAAAAAgCAAAAAAAAMAAAAAAAAAAEAAAA +AQAAAAgAAAAAAAAAGAAAAAAAAAAtAAAAAwAAAAIAAAAAAAAAOAIAAAAAAAA4AgAAAAAAABUAAAAA +AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAANQAAAAEAAAAGAAAAAAAAAAAQAAAAAAAAABAAAAAA +AAALAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAADsAAAABAAAAAgAAAAAAAAAAIAAAAAAA +AAAgAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAABFAAAABgAAAAMAAAAAAAAA +MD8AAAAAAAAwLwAAAAAAANAAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAATgAAAAEAAAAw +AAAAAAAAAAAAAAAAAAAAADAAAAAAAAAsAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEA +AAACAAAAAAAAAAAAAAAAAAAAAAAAADAwAAAAAAAAOAEAAAAAAAAKAAAADAAAAAgAAAAAAAAAGAAA +AAAAAAAJAAAAAwAAAAAAAAAAAAAAAAAAAAAAAABoMQAAAAAAABUAAAAAAAAAAAAAAAAAAAABAAAA +AAAAAAAAAAAAAAAAEQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAfTEAAAAAAABXAAAAAAAAAAAAAAAA +AAAAAQAAAAAAAAAAAAAAAAAAAA== +` diff --git a/tpbase/assembly_decode.go b/tpbase/assembly_decode.go new file mode 100644 index 00000000..403514df --- /dev/null +++ b/tpbase/assembly_decode.go @@ -0,0 +1,93 @@ +/* + * 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 tpbase + +import ( + "errors" + "fmt" + + ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + aa "golang.org/x/arch/arm64/arm64asm" +) + +func arm64GetAnalyzers() []Analyzer { + return []Analyzer{ + {"tls_set", AnalyzeTLSSetARM64}, + } +} + +// AnalyzeTLSSet looks at the assembly of the `tls_set` function in the +// kernel in order to compute the offset of `tp_value` into `task_struct`. +func AnalyzeTLSSetARM64(code []byte) (uint32, error) { + // This tries to extract offset of thread.uw.tp_value relative to + // struct task_struct. The code analyzed comes from: + // linux/arch/arm64/kernel/ptrace.c: tls_set(struct task_struct *target, ...) { + // [...] + // unsigned long tls = target->thread.uw.tp_value; + // + // Anyalysis is based on the fact that 'target' is in X0 at the start, and early + // in the assembly there is a direct load via this pointer. Because of reduced + // instruction set, the pointer often gets moved to another register before the + // load we are interested, so the arg []bool tracks which register is currently + // holding the tracked pointer. Once a proper load is matched, the offset is + // extracted from it. + + // Start tracking of X0 + var arg [32]bool + arg[0] = true + + for offs := 0; offs < len(code); offs += 4 { + inst, err := aa.Decode(code[offs:]) + if err != nil { + break + } + if inst.Op == aa.B { + break + } + + switch inst.Op { + case aa.MOV: + // Track register moves + destReg, ok := ah.Xreg2num(inst.Args[0]) + if !ok { + continue + } + if srcReg, ok := ah.Xreg2num(inst.Args[1]); ok { + arg[destReg] = arg[srcReg] + } + case aa.LDR: + // Track loads with offset of the argument pointer we care + m, ok := inst.Args[1].(aa.MemImmediate) + if !ok { + continue + } + var srcReg int + if srcReg, ok = ah.Xreg2num(m.Base); !ok || !arg[srcReg] { + continue + } + // FIXME: m.imm is not public, but should be. + // https://github.com/golang/go/issues/51517 + imm, ok := ah.DecodeImmediate(m) + if !ok { + return 0, err + } + // Quick sanity check. Per example, the offset should + // be under 4k. But allow some leeway. + if imm < 64 || imm >= 65536 { + return 0, fmt.Errorf("detected tpbase %#x looks invalid", imm) + } + return uint32(imm), nil + default: + // Reset register state if something unsupported happens on it + if destReg, ok := ah.Xreg2num(inst.Args[0]); ok { + arg[destReg] = false + } + } + } + + return 0, errors.New("tp base not found") +} diff --git a/tpbase/assembly_decode_amd64.go b/tpbase/assembly_decode_amd64.go new file mode 100644 index 00000000..b998dee8 --- /dev/null +++ b/tpbase/assembly_decode_amd64.go @@ -0,0 +1,77 @@ +//go:build amd64 + +/* + * 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 tpbase + +import ( + "bytes" + "encoding/binary" + "fmt" + "unsafe" +) + +// #cgo CFLAGS: -g -Wall +// #cgo LDFLAGS: -lZydis +// #include +// #include "fsbase_decode_amd64.h" +import "C" + +func x86GetAnalyzers() []Analyzer { + return []Analyzer{ + {"x86_fsbase_write_task", AnalyzeX86fsbaseWriteTask}, + {"aout_dump_debugregs", AnalyzeAoutDumpDebugregs}, + } +} + +func GetAnalyzers() []Analyzer { + return x86GetAnalyzers() +} + +// AnalyzeAoutDumpDebugregs looks at the assembly of the `aout_dump_debugregs` function in the +// 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") + } + + // Because different compilers generate code that looks different enough, we disassemble the + // function in order to properly analyze the code and deduce the fsbase offset. + // The underlying logic uses the zydis library, hence the cgo call. + offset := uint32(C.decode_fsbase_aout_dump_debugregs( + (*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 offset, nil +} + +// AnalyzeX86fsbaseWriteTask looks at the assembly of the function x86_fsbase_write_task which +// is ideal because it only writes the argument to the fsbase function. We can get the fsbase +// offset directly from the assembly here. Available since kernel version 4.20. +func AnalyzeX86fsbaseWriteTask(code []byte) (uint32, error) { + // Supported sequences (might be surrounded be additional code for the WARN_ONCE): + // + // 1) Alpine Linux (kernel 5.10+) + // 48 89 b7 XX XX XX XX mov %rsi,0xXXXXXXXX(%rdi) + + // No need to disassemble via zydis here, as it's highly unlikely the below machine code + // matching approach would fail. Indeed, x86-64 calling conventions ensure that: + // * %rdi is a pointer to a `task_struct` (first parameter) + // * %rsi == fsbase value (second parameter) + // the x86_fsbase_write_task function simply sets that task (from the first parameter) fsbase to + // be equal to the second parameter. + // 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)") + } + offset := binary.LittleEndian.Uint32(code[idx+3:]) + return offset, nil +} diff --git a/tpbase/assembly_decode_arm64.go b/tpbase/assembly_decode_arm64.go new file mode 100644 index 00000000..3d703d0e --- /dev/null +++ b/tpbase/assembly_decode_arm64.go @@ -0,0 +1,17 @@ +//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. + * See the file "LICENSE" for details. + */ + +package tpbase + +func x86GetAnalyzers() []Analyzer { + return nil +} + +func GetAnalyzers() []Analyzer { + return arm64GetAnalyzers() +} diff --git a/tpbase/assembly_decode_test.go b/tpbase/assembly_decode_test.go new file mode 100644 index 00000000..453a8c56 --- /dev/null +++ b/tpbase/assembly_decode_test.go @@ -0,0 +1,283 @@ +//go:build amd64 + +/* + * 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 tpbase + +import ( + "debug/elf" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFSBase(t *testing.T) { + testCases := map[string]struct { + machine elf.Machine + funcName string + code []byte + fsBase uint32 + }{ + "gcc recent": { + machine: elf.EM_X86_64, + funcName: "aout_dump_debugregs", + // kernels 4.19 -> 5.6 with gcc 8.3 -> 10.0 + // + // 31 c0 xor eax,eax + // 45 31 c0 xor r8d,r8d + // 41 ba 02 00 00 00 mov r10d,0x2 + // 65 4c 8b 0c 25 45 23 01 00 mov r9,QWORD PTR gs:0x12345 + // 49 8b 94 c1 de 0c 00 00 mov rdx,QWORD PTR [r9+rax*8+0xcde] + code: []byte{ + 0x0f, 0x1f, 0x44, 0x00, 0x00, // 5-byte nop from kernel ftrace infrastructure + 0x31, 0xc0, + 0x45, 0x31, 0xc0, + 0x41, 0xba, 0x02, 0x00, 0x00, 0x00, + 0x65, 0x4c, 0x8b, 0x0c, 0x25, 0x45, 0x23, 0x01, 0x00, + 0x49, 0x8b, 0x94, 0xc1, 0xde, 0x0c, 0x00, 0x00, + }, + fsBase: 3278, + }, + "GKE clang9": { + machine: elf.EM_X86_64, + funcName: "aout_dump_debugregs", + // nolint:lll + // Linux version 4.19.112+ (builder@c9d55aaf8a8b) (Chromium OS 9.0_pre361749_p20190714-r4 clang version 9.0.0 (/var/cache/chromeos-cache/distfiles/host/egit-src/llvm-project c11de5eada2decd0a495ea02676b6f4838cd54fb) (based on LLVM 9.0.0svn)) #1 SMP Tue Dec 29 13:50:37 PST 2020 + // + // 55 push rbp + // 48 89 e5 mov rbp,rsp + // 65 48 8b 04 25 00 4d 01 00 mov rax,QWORD PTR gs:0x14d00 + // 48 8b 90 78 0a 00 00 mov rdx,QWORD PTR [rax+0xa78] + code: []byte{ + 0x55, + 0x48, 0x89, 0xe5, + 0x65, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x4d, 0x01, 0x00, + 0x48, 0x8b, 0x90, 0x78, 0x0a, 0x00, 0x00, + }, + fsBase: 2664, + }, + "Amazon linux": { + machine: elf.EM_X86_64, + funcName: "aout_dump_debugregs", + // Kernel 4.14, gcc 7.3 + // + // 31 c0 xor eax,eax + // 31 f6 xor esi,esi + // 41 b9 02 00 00 00 mov r9d,0x2 + // 65 4c 8b 04 25 56 34 02 00 mov r8,QWORD PTR gs:0x23456 + // 49 8b 94 c0 ea 0d 00 00 mov rdx,QWORD PTR [r8+rax*8+0xdea] + code: []byte{ + 0x31, 0xc0, + 0x31, 0xf6, + 0x41, 0xb9, 0x02, 0x00, 0x00, 0x00, + 0x65, 0x4c, 0x8b, 0x04, 0x25, 0x56, 0x34, 0x02, 0x00, + 0x49, 0x8b, 0x94, 0xc0, 0xea, 0x0d, 0x00, 0x00, + }, + fsBase: 3546, + }, + "Ubuntu Bionic": { + machine: elf.EM_X86_64, + funcName: "aout_dump_debugregs", + // kernel 4.15, gcc 7.5 + // + // 55 push rbp + // 31 c0 xor eax,eax + // 31 f6 xor esi,esi + // 41 b9 02 00 00 00 mov r9d,0x2 + // 48 89 e5 mov rbp,rsp + // 65 4c 8b 04 25 34 12 00 00 mov r8,QWORD PTR gs:0x1234 + // 49 8b 94 c0 aa 0a 00 00 mov rdx,QWORD PTR [r8+rax*8+0xaaa] + code: []byte{ + 0x55, + 0x31, 0xc0, + 0x31, 0xf6, + 0x41, 0xb9, 0x02, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xe5, + 0x65, 0x4c, 0x8b, 0x04, 0x25, 0x34, 0x12, 0x00, 0x00, + 0x49, 0x8b, 0x94, 0xc0, 0xaa, 0x0a, 0x00, 0x00, + }, + fsBase: 2714, + }, + "Ubuntu Focal Fossa (AWS)": { + machine: elf.EM_X86_64, + funcName: "aout_dump_debugregs", + // kernel 5.4.0-1029-aws + // + // 55 push rbp + // 31 c0 xor eax,eax + // 45 31 c0 xor r8d,r8d + // 41 ba 02 00 00 00 mov r10d,0x2 + // 65 4c 8b 0c 25 c0 6b 01 00 mov r9,QWORD PTR gs:0x16bc0 + // 48 89 e5 mov rbp,rsp + // 49 8b 94 c1 38 13 00 00 mov rdx,QWORD PTR [r9+rax*8+0x1338] + code: []byte{ + 0x55, + 0x31, 0xc0, + 0x45, 0x31, 0xc0, + 0x41, 0xba, 0x02, 0x00, 0x00, 0x00, + 0x65, 0x4c, 0x8b, 0x0c, 0x25, 0xc0, 0x6b, 0x01, 0x00, + 0x48, 0x89, 0xe5, + 0x49, 0x8b, 0x94, 0xc1, 0x38, 0x13, 0x00, 0x00, + }, + fsBase: 4904, + }, + "RHEL / gcc 4.8.5-39": { + machine: elf.EM_X86_64, + funcName: "aout_dump_debugregs", + // from booking.com + // 55 push rbp + // be 10 00 00 00 mov esi,0x10 + // 31 c0 xor eax,eax + // 65 48 8b 14 25 80 5c 01 00 mov rdx,QWORD PTR gs:0x15c80 + // 4c 8d 8a c0 12 00 00 lea r9,[rdx+0x12c0] + // 45 31 c0 xor r8d,r8d + // 41 ba 02 00 00 00 mov r10d,0x2 + // 48 89 e5 mov rbp,rsp + // 49 8b 54 c1 38 mov rdx,QWORD PTR [r9+rax*8+0x38] + // 48 85 d2 test rdx,rdx + code: []byte{ + 0x55, + 0xbe, 0x10, 0x00, 0x00, 0x00, + 0x31, 0xc0, + 0x65, 0x48, 0x8b, 0x14, 0x25, 0x80, 0x5c, 0x01, 0x00, + 0x4c, 0x8d, 0x8a, 0xc0, 0x12, 0x00, 0x00, + 0x45, 0x31, 0xc0, + 0x41, 0xba, 0x02, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xe5, + 0x49, 0x8b, 0x54, 0xc1, 0x38, + 0x48, 0x85, 0xd2, + }, + fsBase: 4840, + }, + "AL / Kernel 5.10+": { + machine: elf.EM_X86_64, + funcName: "x86_fsbase_write_task", + // Extracted from Alpine Linux (kernel 5.10+), but should be similar on all + // kernels 4.20+ due to x86-64 calling conventions. + // + // 48 89 b7 2a 0f 00 00 mov QWORD PTR [rdi+0xf2a],rsi + code: []byte{ + 0x48, 0x89, 0xb7, 0x2a, 0x0f, 0x00, 0x00, + }, + fsBase: 3882, + }, + "tls_set / arm64": { + machine: elf.EM_AARCH64, + funcName: "tls_set", + // HINT #0x22 + // MOV X9, X30 + // NOP + // HINT #0x19 + // STP X29, X30, [SP,#-64]! + // MRS X1, S3_0_C4_C1_0 + // MOV X29, SP + // STP X21, X22, [SP,#32] + // MOV X21, X0 1. Register X0 moved to X21 + // LDR X0, [X1,#1816] + // STR X0, [SP,#56] + // MOV X0, #0x0 + // LDR X1, [X21,#3440] 2. #3440 is the offset we want + code: []byte{ + 0x5f, 0x24, 0x03, 0xd5, 0xe9, 0x03, 0x1e, 0xaa, + 0x1f, 0x20, 0x03, 0xd5, 0x3f, 0x23, 0x03, 0xd5, + 0xfd, 0x7b, 0xbc, 0xa9, 0x01, 0x41, 0x38, 0xd5, + 0xfd, 0x03, 0x00, 0x91, 0xf5, 0x5b, 0x02, 0xa9, + 0xf5, 0x03, 0x00, 0xaa, 0x20, 0x8c, 0x43, 0xf9, + 0xe0, 0x1f, 0x00, 0xf9, 0x00, 0x00, 0x80, 0xd2, + 0xa1, 0xba, 0x46, 0xf9, 0xe1, 0x1b, 0x00, 0xf9, + 0x83, 0x01, 0x00, 0x34, 0xf3, 0x53, 0x01, 0xa9, + }, + fsBase: 3440, + }, + "Alpine 3.18.4 EC2 / arm64": { + machine: elf.EM_AARCH64, + funcName: "tls_set", + // HINT #0x19 + // STP X29, X30, [SP,#-96]! + // MRS X6, S3_0_C4_C1_0 + // MOV X29, SP + // STP X19, X20, [SP,#16] + // MOV X20, X0 + // MOV W19, W3 + // STP X21, X22, [SP,#32] + // MOV W21, W2 + // MOV X22, X4 + // STR X23, [SP,#48] + // MOV X23, X5 + // LDR X0, [X6,#1896] + // STR X0, [SP,#88] + // MOV X0, #0x0 + // STP XZR, XZR, [SP,#72] + // LDR X3, [X20,#3632] + code: []byte{ + 0x3f, 0x23, 0x03, 0xd5, 0xfd, 0x7b, 0xba, 0xa9, + 0x06, 0x41, 0x38, 0xd5, 0xfd, 0x03, 0x00, 0x91, + 0xf3, 0x53, 0x01, 0xa9, 0xf4, 0x03, 0x00, 0xaa, + 0xf3, 0x03, 0x03, 0x2a, 0xf5, 0x5b, 0x02, 0xa9, + 0xf5, 0x03, 0x02, 0x2a, 0xf6, 0x03, 0x04, 0xaa, + 0xf7, 0x1b, 0x00, 0xf9, 0xf7, 0x03, 0x05, 0xaa, + 0xc0, 0xb4, 0x43, 0xf9, 0xe0, 0x2f, 0x00, 0xf9, + 0x00, 0x00, 0x80, 0xd2, 0xff, 0xff, 0x04, 0xa9, + 0x83, 0x1a, 0x47, 0xf9, 0xe3, 0x27, 0x00, 0xf9, + }, + fsBase: 3632, + }, + "Linux 6.5.11 compiled with LLVM-17": { + // nolint:lll + // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=v6.5.11&id=799441832db16b99e400ccbec55db801e6992819 + machine: elf.EM_AARCH64, + funcName: "tls_set", + code: []byte{ + 0x3f, 0x23, 0x03, 0xd5, // paciasp + 0xff, 0x83, 0x01, 0xd1, // sub sp, sp, #0x60 + 0xfd, 0x7b, 0x03, 0xa9, // stp x29, x30, [sp, #48] + 0xf6, 0x57, 0x04, 0xa9, // stp x22, x21, [sp, #64] + 0xf4, 0x4f, 0x05, 0xa9, // stp x20, x19, [sp, #80] + 0xfd, 0xc3, 0x00, 0x91, // add x29, sp, #0x30 + 0x08, 0x41, 0x38, 0xd5, // mrs x8, sp_el0 + 0xf5, 0x03, 0x05, 0xaa, // mov x21, x5 + 0x08, 0x31, 0x45, 0xf9, // ldr x8, [x8, #2656] + 0xf4, 0x03, 0x04, 0xaa, // mov x20, x4 + 0xf3, 0x03, 0x00, 0xaa, // mov x19, x0 + 0xa8, 0x83, 0x1f, 0xf8, // stur x8, [x29, #-8] + 0x08, 0x98, 0x45, 0xf9, // ldr x8, [x0, #2864] + 0xe8, 0xff, 0x01, 0xa9, // stp x8, xzr, [sp, #24] + 0x1f, 0x20, 0x03, 0xd5, // nop + 0x63, 0x02, 0x00, 0x34, // cbz w3, 0x1c630 + }, + fsBase: 2864, + }, + } + + for name, test := range testCases { + name := name + test := test + t.Run(name, func(t *testing.T) { + var analyzers []Analyzer + switch test.machine { + case elf.EM_X86_64: + analyzers = x86GetAnalyzers() + case elf.EM_AARCH64: + analyzers = arm64GetAnalyzers() + } + if analyzers == nil { + t.Skip("tests not available on this platform") + } + for _, a := range analyzers { + if a.FunctionName != test.funcName { + continue + } + fsBase, err := a.Analyze(test.code) + if assert.NoError(t, err) { + assert.Equal(t, test.fsBase, fsBase, "Wrong fsbase extraction") + } + return + } + t.Errorf("no extractor for '%s'", test.funcName) + }) + } +} diff --git a/tpbase/fsbase_decode_amd64.c b/tpbase/fsbase_decode_amd64.c new file mode 100644 index 00000000..3a589597 --- /dev/null +++ b/tpbase/fsbase_decode_amd64.c @@ -0,0 +1,124 @@ +//go:build amd64 + +#include +#include "fsbase_decode_amd64.h" + + +// decode_fsbase_aout_dump_debugregs attempts to compute the offset of `fsbase` in `task_struct` from the x86-64 +// assembly code of the `aout_dump_debugregs` function in the kernel, which existed up until kernel 5.9. +// It returns the fsbase offset if successful, or 0 on failure. +// aout_dump_debugregs code: see https://elixir.bootlin.com/linux/v5.9.16/source/arch/x86/kernel/hw_breakpoint.c#L452 +// +// This function expects 2 instructions to be present in the code blob: +// 1) A `mov` instruction loading the current task_struct address (recognizable with the GS segment being the base) into +// a target register. +// 2) A subsequent `mov` instruction loading the address of `task_struct->thread.ptrace_bps[i]`, the base register being +// the target register of the previous instruction. +// +// From 2) we can extract the offset of ptrace_bps in task_struct. +// The layout of `task_struct.thread` (see arch/x86/include/asm/processor.h) is: +// [...] +// unsigned long fsbase; +// unsigned long gsbase; +// struct perf_event *ptrace_bps[HBP_NUM]; +// [...] +// => we can then subtract 2*sizeof(unsigned long) to find the fsbase offset. +uint32_t decode_fsbase_aout_dump_debugregs(const uint8_t* code, size_t codesz) { + ZydisDecoder decoder; + ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64); + + ZydisDecodedInstruction instr; + ZydisRegister target_register = ZYDIS_REGISTER_NONE; + + ZyanUSize instruction_offset = 0; + + // 1) Find the first `mov` with a `gs` base. By inspection of the C code, we assume it loads the address of the + // current `task_struct`. + while (ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + instruction_offset, codesz - instruction_offset, &instr))) { + instruction_offset += instr.length; + + if (! (instr.attributes & ZYDIS_ATTRIB_HAS_SEGMENT_GS)) { + continue; + } + if (instr.mnemonic != ZYDIS_MNEMONIC_MOV) { + continue; + } + if (instr.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER) { + continue; + } + // This instruction loads the address of the current task_struct into `target_register`. + target_register = instr.operands[0].reg.value; + break; + } + + if (target_register == ZYDIS_REGISTER_NONE) { + return 0; + } + + int64_t lea_offset = 0; + int64_t mov_offset = 0; + + // 2) Find the first `mov` instruction that either uses `target_register` as base, or for which the base register is + // the result of a LEA that uses `target_register` as base. + // We assume that `mov` computes the address of `task_struct.thread.ptrace_bps` based on the `task_struct` address + // we expect to have loaded in 1). + while (ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + instruction_offset, codesz - instruction_offset, &instr))) { + instruction_offset += instr.length; + + // Some compilers will emit LEA+MOV instead of MOV. + // In this case, we need to add offsets from both. + if (instr.mnemonic == ZYDIS_MNEMONIC_LEA) { + if (instr.operands[1].type != ZYDIS_OPERAND_TYPE_MEMORY) { + continue; + } + if (instr.operands[1].mem.base != target_register) { + continue; + } + if (lea_offset != 0) { + // We already found a matching LEA. A second one means we went too far. + return 0; + } + if (! instr.operands[1].mem.disp.has_displacement) { + return 0; + } + if (instr.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER) { + return 0; + } + // Update target register to be this LEA's target + target_register = instr.operands[0].reg.value; + + lea_offset = instr.operands[1].mem.disp.value; + continue; + } + + if (instr.mnemonic == ZYDIS_MNEMONIC_MOV) { + if (instr.operands[1].type != ZYDIS_OPERAND_TYPE_MEMORY) { + continue; + } + if (instr.operands[1].mem.base != target_register) { + continue; + } + if (! instr.operands[1].mem.disp.has_displacement) { + return 0; + } + // The displacement is the offset of ptrace_bps in task_struct, minus any offset from a previous LEA instruction. + mov_offset = instr.operands[1].mem.disp.value; + break; + } + } + + if (mov_offset == 0) { + return 0; + } + + int64_t result = lea_offset + mov_offset; + + // Compute the `fsbase` offset from the `ptrace_bps` offset, according to the `thread_struct` layout. + result -= 2*sizeof(long); + + if (result < 0 || result > UINT32_MAX) { + return 0; + } + + return (uint32_t)result; +} diff --git a/tpbase/fsbase_decode_amd64.h b/tpbase/fsbase_decode_amd64.h new file mode 100644 index 00000000..5208cf2c --- /dev/null +++ b/tpbase/fsbase_decode_amd64.h @@ -0,0 +1,10 @@ +//go:build amd64 + +#ifndef __FSBASE_DECODE_X86_64__ +#define __FSBASE_DECODE_X86_64__ + +#include + +uint32_t decode_fsbase_aout_dump_debugregs(const uint8_t* code, size_t codesz); + +#endif diff --git a/tpbase/libc.go b/tpbase/libc.go new file mode 100644 index 00000000..10ea8f45 --- /dev/null +++ b/tpbase/libc.go @@ -0,0 +1,353 @@ +/* + * 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 tpbase + +import ( + "errors" + "fmt" + "regexp" + + ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/libpf/stringutil" + + aa "golang.org/x/arch/arm64/arm64asm" +) + +// TSDInfo contains information to access C-library's Thread Specific Data from eBPF +type TSDInfo struct { + // Offset is the pointer difference from "tpbase" pointer to the C-library + // specific struct pthread's member containing the thread specific data: + // .tsd (musl) or .specific (glibc). + // Note: on x86_64 it's positive value, and arm64 it is negative value as + // "tpbase" register has different purpose and pointer value per platform ABI. + Offset int16 + + // Multiplier is the TSD specific value array element size. + // Typically 8 bytes on 64bit musl and 16 bytes on 64bit glibc + Multiplier uint8 + + // Indirect is a flag indicating if the "tpbase + Offset" points to a member + // which is a pointer the array (musl) and not the array itself (glibc). + Indirect uint8 +} + +// This code analyzes the C-library provided POSIX defined function which is used +// to read thread-specific data (TSD): +// void *pthread_getspecific(pthread_key_t key); +// +// The actual symbol and its location is C-library specific: +// +// LIBC DSO Symbol +// musl/alpine ld-musl-$ARCH.so.1 pthread_getspecific +// musl/generic libc.musl-$ARCH.so.1 pthread_getspecific +// glibc/new libc.so.6 __pthread_getspecific +// glibc/old libpthread.so.0 __pthread_getspecific + +// musl: +// http://git.musl-libc.org/cgit/musl/tree/src/internal/pthread_impl.h?h=v1.2.3#n49 +// http://git.musl-libc.org/cgit/musl/tree/src/thread/pthread_getspecific.c?h=v1.2.3#n4 +// +// struct pthread { +// ... +// void **tsd; +// ... +// }; +// +// The implementation is just "return self->tsd[key];". We do the same. + +// glibc: +// https://sourceware.org/git/?p=glibc.git;a=blob;f=nptl/descr.h;hb=c804cd1c00ad#l307 +// https://sourceware.org/git/?p=glibc.git;a=blob;f=nptl/pthread_getspecific.c;hb=c804cd1c00ad#l23 +// +// struct pthread { +// ... +// struct pthread_key_data { +// uintptr_t seq; +// void *data; +// } specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE]; +// struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE]; +// ... +// } +// +// The 1st block is special cased for keys smaller than PTHREAD_KEY_2NDLEVEL_SIZE. +// We also assume we don't see large keys, and support only the small key case. +// Further both x86_64 and arm64 disassembler assume that small key code is the +// main code flow (as in, any conditional jumps are not followed). +// +// Reading the value is basically "return self->specific_1stblock[key].data;" + +var ( + // regex for the libc + libcRegex = regexp.MustCompile(`.*/(ld-musl|libc|libpthread)([-.].*)?\.so`) + + // error that a non-native architectures is not implemented (to skip tests) + errArchNotImplemented = errors.New("architecture not implemented") +) + +// IsPotentialTSDDSO determines if the DSO filename potentially contains pthread code +func IsPotentialTSDDSO(filename string) bool { + return libcRegex.MatchString(filename) +} + +// ExtractTSDInfo extracts the introspection data for pthread thread specific data. +func ExtractTSDInfo(ef *pfelf.File) (*TSDInfo, error) { + sym, err := ef.LookupSymbol("__pthread_getspecific") + if err != nil { + sym, err = ef.LookupSymbol("pthread_getspecific") + if err != nil { + return nil, fmt.Errorf("no getspecific function: %s", err) + } + } + if sym.Size < 8 { + return nil, fmt.Errorf("getspecific function size is %d", sym.Size) + } + + code := make([]byte, sym.Size) + if _, err = ef.ReadVirtualMemory(code, int64(sym.Address)); err != nil { + return nil, fmt.Errorf("failed to read getspecific function: %s", err) + } + + info, err := ExtractTSDInfoNative(code) + if err != nil { + return nil, fmt.Errorf("failed to extract getspecific data: %s", err) + } + + return &info, nil +} + +const ( + Unspec int = iota + TSDBase + TSDElementBase + TSDIndex + TSDValue +) + +type regState struct { + status int + offset int + multiplier int + indirect bool +} + +func ExtractTSDInfoARM64(code []byte) (TSDInfo, error) { + // This tries to extract offsetof(struct pthread, tsd). + // The analyzed code is pthread_getspecific, and should work on glibc and musl. + // See test cases for example assembly. The strategy is to find "MRS xx, tpidr_el0" + // instruction as loading something relative to "struct pthread". It is + // then tracked against first argument to find the exact offset and multiplier + // to address the TSD array. + + // Start tracking of X0 + var regs [32]regState + + regs[0].status = TSDIndex + regs[0].multiplier = 1 + resetReg := int(-1) + + for offs := 0; offs < len(code); offs += 4 { + if resetReg >= 0 { + // Reset register state if something unsupported happens on it + regs[resetReg] = regState{status: Unspec} + } + + inst, err := aa.Decode(code[offs:]) + if err != nil { + continue + } + if inst.Op == aa.RET { + break + } + + destReg, ok := ah.Xreg2num(inst.Args[0]) + if !ok { + continue + } + + resetReg = destReg + switch inst.Op { + case aa.MOV: + // Track register moves + srcReg, ok := ah.Xreg2num(inst.Args[1]) + if !ok { + continue + } + regs[destReg] = regs[srcReg] + case aa.MRS: + // MRS X1, S3_3_C13_C0_2 + if inst.Args[1].String() == "S3_3_C13_C0_2" { + regs[destReg] = regState{ + status: TSDBase, + multiplier: 1, + } + } + case aa.LDUR: + // LDUR X1, [X1,#-88] + m, ok := inst.Args[1].(aa.MemImmediate) + if !ok { + continue + } + srcReg, ok := ah.Xreg2num(m.Base) + if !ok { + continue + } + if regs[srcReg].status == TSDBase { + imm, ok := ah.DecodeImmediate(m) + if !ok { + continue + } + regs[destReg] = regState{ + status: TSDBase, + offset: regs[srcReg].offset + int(imm), + multiplier: regs[srcReg].multiplier, + indirect: true, + } + } else { + continue + } + case aa.LDR: + switch m := inst.Args[1].(type) { + case aa.MemExtend: + // LDR X0, [X1,W0,UXTW #3] + srcReg, ok := ah.Xreg2num(m.Base) + if !ok { + continue + } + srcIndex, ok := ah.Xreg2num(m.Index) + if !ok { + continue + } + if regs[srcReg].status == TSDBase && regs[srcIndex].status == TSDIndex { + regs[destReg] = regState{ + status: TSDValue, + offset: regs[srcReg].offset + (regs[srcIndex].offset << m.Amount), + multiplier: regs[srcReg].multiplier << m.Amount, + indirect: regs[srcReg].indirect, + } + } else { + continue + } + case aa.MemImmediate: + // ldr x0, [x2, #8] + srcReg, ok := ah.Xreg2num(m.Base) + if !ok { + continue + } + if regs[srcReg].status == TSDElementBase { + i, ok := ah.DecodeImmediate(m) + if !ok { + continue + } + regs[destReg] = regState{ + status: TSDValue, + offset: regs[srcReg].offset + int(i), + multiplier: regs[srcReg].multiplier, + indirect: regs[srcReg].indirect, + } + } else { + continue + } + } + case aa.UBFIZ: + // UBFIZ X0, X1, #4, #32 + srcReg, ok := ah.Xreg2num(inst.Args[1]) + if !ok { + continue + } + if regs[srcReg].status == TSDIndex { + i, ok := inst.Args[2].(aa.Imm) + if !ok { + continue + } + regs[destReg] = regState{ + status: TSDIndex, + offset: regs[srcReg].offset << i.Imm, + multiplier: regs[srcReg].multiplier << i.Imm, + } + } + case aa.ADD: + srcReg, ok := ah.Xreg2num(inst.Args[1]) + if !ok { + continue + } + switch a2 := inst.Args[2].(type) { + case aa.ImmShift: + i, ok := ah.DecodeImmediate(a2) + if !ok { + continue + } + regs[destReg] = regs[srcReg] + regs[destReg].offset += int(i) + case aa.RegExtshiftAmount: + regStr := inst.Args[2].String() + shift := int(0) + var fields [2]string + if stringutil.SplitN(regStr, ",", fields[:]) == 2 { + regStr = fields[0] + n, err := fmt.Sscanf(fields[1], " LSL #%v", &shift) + if n != 1 || err != nil { + continue + } + } + reg, ok := ah.DecodeRegister(regStr) + if !ok { + continue + } + srcReg2, ok := ah.Xreg2num(aa.Reg(reg)) + if !ok { + continue + } + if regs[srcReg].status == TSDBase && regs[srcReg2].status == TSDIndex { + regs[destReg] = regState{ + status: TSDElementBase, + offset: regs[srcReg].offset + regs[srcReg2].offset< +// #include "libc_decode_amd64.h" +import "C" + +func ExtractTSDInfoX64_64(code []byte) (TSDInfo, error) { + // function in order to properly analyze the code and deduce the fsbase offset. + // The underlying logic uses the zydis library, hence the cgo call. + val := uint32(C.decode_pthread_getspecific( + (*C.uint8_t)(unsafe.Pointer(&code[0])), C.size_t(len(code)))) + + if val == 0 { + return TSDInfo{}, errors.New("unable to determine libc info") + } + + return TSDInfo{ + Offset: int16(val & 0xffff), + Multiplier: uint8(val >> 16), + Indirect: uint8((val >> 24) & 1), + }, nil +} + +func ExtractTSDInfoNative(code []byte) (TSDInfo, error) { + return ExtractTSDInfoX64_64(code) +} diff --git a/tpbase/libc_arm64.go b/tpbase/libc_arm64.go new file mode 100644 index 00000000..5ef17753 --- /dev/null +++ b/tpbase/libc_arm64.go @@ -0,0 +1,17 @@ +//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. + * See the file "LICENSE" for details. + */ + +package tpbase + +func ExtractTSDInfoX64_64(code []byte) (TSDInfo, error) { + return TSDInfo{}, errArchNotImplemented +} + +func ExtractTSDInfoNative(code []byte) (TSDInfo, error) { + return ExtractTSDInfoARM64(code) +} diff --git a/tpbase/libc_decode_amd64.c b/tpbase/libc_decode_amd64.c new file mode 100644 index 00000000..c1d88a4c --- /dev/null +++ b/tpbase/libc_decode_amd64.c @@ -0,0 +1,179 @@ +//go:build amd64 + +#include +#include "libc_decode_amd64.h" + +//#define DEBUG + +#ifdef DEBUG +#include +#endif + +#define MAX(a, b) ((a)>(b) ? (a) : (b)) + +enum { + Unspec = 0, + TSDBase, + TSDElementBase, + TSDIndex, + TSDValue, +}; + +typedef struct regInfo { + uint8_t state; + uint8_t multiplier; + uint8_t indirect; + int16_t offset; +} regInfo; + +static int32_t reg2ndx(ZydisRegister reg) +{ + reg = ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg); + if (reg >= ZYDIS_REGISTER_RAX && reg <= ZYDIS_REGISTER_R15) + return reg - ZYDIS_REGISTER_RAX + 1; + return 0; +} + +uint32_t decode_pthread_getspecific(const uint8_t* code, size_t codesz) { + ZydisDecoder decoder; + ZydisDecodedInstruction instr; + regInfo regs[18] = {}; + int32_t destNdx = -1, srcNdx, indexNdx; + + // RDI = first argument = key index + regs[reg2ndx(ZYDIS_REGISTER_RDI)] = (regInfo) { .state = TSDIndex, .multiplier = 1 }; + + ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64); + + for (ZyanUSize offs = 0; ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(&decoder, code + offs, codesz - offs, &instr)); offs += instr.length) { +#ifdef DEBUG + if (destNdx >= 0 && destNdx < 32) { + fprintf(stderr, "r%02d state=%d, offs=%#x, mult=%d\n", + destNdx, regs[destNdx].state, regs[destNdx].offset, regs[destNdx].multiplier); + } +#endif + destNdx = -1; + if (instr.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER) { + continue; + } + + destNdx = reg2ndx(instr.operands[0].reg.value); + switch (instr.mnemonic) { + case ZYDIS_MNEMONIC_SHL: + regs[destNdx].offset <<= instr.operands[1].imm.value.u; + regs[destNdx].multiplier <<= instr.operands[1].imm.value.u; + continue; + + case ZYDIS_MNEMONIC_ADD: + if ((instr.attributes & ZYDIS_ATTRIB_HAS_SEGMENT_FS) && + regs[destNdx].state == TSDIndex) { + regs[destNdx].state = TSDElementBase; + continue; + } + if (instr.operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER) { + srcNdx = reg2ndx(instr.operands[1].reg.value); + if ((regs[destNdx].state == TSDBase && regs[srcNdx].state == TSDIndex) || + (regs[destNdx].state == TSDIndex && regs[srcNdx].state == TSDBase)) { + regs[destNdx].offset += regs[srcNdx].offset; + // The register in TSDBase state has multiplier unset. This selects the + // multiplier of TSDIndex register. + regs[destNdx].multiplier = MAX(regs[destNdx].multiplier, regs[srcNdx].multiplier); + regs[destNdx].state = TSDElementBase; + continue; + } + } else if (instr.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) { + regs[destNdx].offset += instr.operands[1].imm.value.u; + continue; + } + break; + + case ZYDIS_MNEMONIC_LEA: + srcNdx = reg2ndx(instr.operands[1].mem.base); + if (regs[srcNdx].state == TSDIndex) { + if (instr.operands[1].mem.index == ZYDIS_REGISTER_NONE) { + regs[destNdx] = (regInfo) { + .state = TSDIndex, + .offset = regs[srcNdx].offset + instr.operands[1].mem.disp.value, + .multiplier = regs[srcNdx].multiplier, + }; + continue; + } + } else if (regs[srcNdx].state == TSDBase) { + indexNdx = reg2ndx(instr.operands[1].mem.index); + if (regs[indexNdx].state == TSDIndex) { + regs[destNdx] = (regInfo) { + .state = TSDElementBase, + .offset = regs[srcNdx].offset + regs[indexNdx].offset + instr.operands[1].mem.disp.value, + .multiplier = regs[indexNdx].multiplier * (instr.operands[1].mem.scale ?: 1), + }; + continue; + } + } + break; + + case ZYDIS_MNEMONIC_MOV: + if (instr.attributes & ZYDIS_ATTRIB_HAS_SEGMENT_FS) { + regs[destNdx] = (regInfo) { .state = TSDBase }; + continue; + } + if (instr.operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER) { + srcNdx = reg2ndx(instr.operands[1].reg.value); + regs[destNdx] = regs[srcNdx]; + continue; + } + if (instr.operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY) { + srcNdx = reg2ndx(instr.operands[1].mem.base); + indexNdx = reg2ndx(instr.operands[1].mem.index); + if (regs[srcNdx].state == TSDBase) { + if (instr.operands[1].mem.index == ZYDIS_REGISTER_NONE) { + regs[destNdx] = (regInfo) { + .state = TSDBase, + .offset = instr.operands[1].mem.disp.value, + .indirect = 1, + }; + continue; + } else if (regs[indexNdx].state == TSDIndex) { + regs[destNdx] = (regInfo) { + .state = TSDValue, + .offset = regs[srcNdx].offset, + .indirect = regs[srcNdx].indirect, + .multiplier = instr.operands[1].mem.scale, + }; + continue; + } + } else if (regs[srcNdx].state == TSDElementBase) { + regs[destNdx] = (regInfo) { + .state = TSDValue, + .offset = regs[srcNdx].offset + instr.operands[1].mem.disp.value, + .indirect = regs[srcNdx].indirect, + .multiplier = regs[srcNdx].multiplier * (instr.operands[1].mem.scale ?: 1), + }; + continue; + } + } + break; + + case ZYDIS_MNEMONIC_RET: + // Return value is in RAX + srcNdx = reg2ndx(ZYDIS_REGISTER_RAX); + if (regs[srcNdx].state != TSDValue) + return 0; + + return (uint16_t)regs[srcNdx].offset | + ((uint32_t)regs[srcNdx].multiplier << 16) | + ((uint32_t)regs[srcNdx].indirect << 24); + + case ZYDIS_MNEMONIC_CMP: + case ZYDIS_MNEMONIC_TEST: + // Opcodes without effect to destNdx. + continue; + + default: + break; + } + + // Unsupported opcode. Assume it modified the operand 0, and mark it unknown. + regs[destNdx] = (regInfo) { .state = Unspec }; + } + return 0; +} diff --git a/tpbase/libc_decode_amd64.h b/tpbase/libc_decode_amd64.h new file mode 100644 index 00000000..3d7452b5 --- /dev/null +++ b/tpbase/libc_decode_amd64.h @@ -0,0 +1,10 @@ +//go:build amd64 + +#ifndef LIBC_DECODE_X86_64 +#define LIBC_DECODE_X86_64 + +#include + +uint32_t decode_pthread_getspecific(const uint8_t* code, size_t codesz); + +#endif diff --git a/tpbase/libc_test.go b/tpbase/libc_test.go new file mode 100644 index 00000000..60ee88e9 --- /dev/null +++ b/tpbase/libc_test.go @@ -0,0 +1,238 @@ +/* + * 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 tpbase + +import ( + "debug/elf" + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractTSDInfo(t *testing.T) { + testCases := map[string]struct { + machine elf.Machine + code []byte + info TSDInfo + }{ + "musl 1.2.3 / Alpine 3.16 / arm64": { + machine: elf.EM_AARCH64, + code: []byte{ + 0x41, 0xd0, 0x3b, 0xd5, // mrs x1, tpidr_el0 + 0x21, 0x80, 0x5a, 0xf8, // ldur x1, [x1, #-88] + 0x20, 0x58, 0x60, 0xf8, // ldr x0, [x1, w0, uxtw #3] + 0xc0, 0x03, 0x5f, 0xd6, // ret + }, + info: TSDInfo{ + Offset: -88, + Multiplier: 8, + Indirect: 1, + }, + }, + "glibc 2.35 / Fedora 36 / arm64": { + machine: elf.EM_AARCH64, + code: []byte{ + 0x5f, 0x24, 0x03, 0xd5, // bti c + 0xe1, 0x03, 0x00, 0x2a, // mov w1, w0 + 0x1f, 0x7c, 0x00, 0x71, // cmp w0, #0x1f + 0x48, 0x02, 0x00, 0x54, // b.hi 85bb4 <__pthread_getspecific+0x54> + 0x20, 0x7c, 0x7c, 0xd3, // ubfiz x0, x1, #4, #32 + 0x42, 0xd0, 0x3b, 0xd5, // mrs x2, tpidr_el0 + 0x00, 0xc0, 0x1a, 0xd1, // sub x0, x0, #0x6b0 + 0x42, 0x00, 0x00, 0x8b, // add x2, x2, x0 + 0x40, 0x04, 0x40, 0xf9, // ldr x0, [x2, #8] + 0x40, 0x01, 0x00, 0xb4, // cbz x0, 85bac <__pthread_getspecific+0x4c> + 0x21, 0x7c, 0x7c, 0xd3, // ubfiz x1, x1, #4, #32 + 0xe3, 0x08, 0x00, 0xd0, // adrp x3, 1a3000 + 0x63, 0x40, 0x0a, 0x91, // add x3, x3, #0x290 + 0x44, 0x00, 0x40, 0xf9, // ldr x4, [x2] + 0x61, 0x68, 0x61, 0xf8, // ldr x1, [x3, x1] + 0x3f, 0x00, 0x04, 0xeb, // cmp x1, x4 + 0x41, 0x00, 0x00, 0x54, // b.ne 85ba8 <__pthread_getspecific+0x48> + 0xc0, 0x03, 0x5f, 0xd6, // ret + // code skipped handling keys >0x1f + }, + info: TSDInfo{ + Offset: -0x6b0 + 8, + Multiplier: 0x10, + }, + }, + "glibc 2.33 / Fedora 34 / arm64": { + machine: elf.EM_AARCH64, + code: []byte{ + 0x5f, 0x24, 0x03, 0xd5, // bti c + 0xe1, 0x03, 0x00, 0x2a, // mov w1, w0 + 0x1f, 0x7c, 0x00, 0x71, // cmp w0, #0x1f + 0x48, 0x02, 0x00, 0x54, // b.hi fb94 <__pthread_getspecific+0x54> // b.pmore + 0x40, 0xd0, 0x3b, 0xd5, // mrs x0, tpidr_el0 + 0x22, 0x44, 0x00, 0x11, // add w2, w1, #0x11 + 0x00, 0x40, 0x1e, 0xd1, // sub x0, x0, #0x790 + 0x02, 0x10, 0x02, 0x8b, // add x2, x0, x2, lsl #4 + 0x40, 0x04, 0x40, 0xf9, // ldr x0, [x2, #8] + 0x00, 0x01, 0x00, 0xb4, // cbz x0, fb84 <__pthread_getspecific+0x44> + 0x21, 0x7c, 0x7c, 0xd3, // ubfiz x1, x1, #4, #32 + 0x03, 0x01, 0x00, 0xb0, // adrp x3, 30000 <__nptl_nthreads> + 0x63, 0x80, 0x01, 0x91, // add x3, x3, #0x60 + 0x44, 0x00, 0x40, 0xf9, // ldr x4, [x2] + 0x61, 0x68, 0x61, 0xf8, // ldr x1, [x3, x1] + 0x3f, 0x00, 0x04, 0xeb, // cmp x1, x4 + 0x41, 0x00, 0x00, 0x54, // b.ne fb88 <__pthread_getspecific+0x48> // b.any + 0xc0, 0x03, 0x5f, 0xd6, // ret + // code skipped handling keys >0x1f + }, + info: TSDInfo{ + Offset: -0x790 + (0x11 << 4) + 8, + Multiplier: 0x10, + }, + }, + "musl 1.2.3 / Alpine 3.16 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // mov %fs:0x0,%rax + // mov 0x80(%rax),%rax + // mov %edi,%edi + // mov (%rax,%rdi,8),%rax + // ret + 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x8b, 0x80, 0x80, 0x00, 0x00, 0x00, + 0x89, 0xff, 0x48, 0x8b, 0x04, 0xf8, 0xc3, + }, + info: TSDInfo{ + Offset: 0x80, + Multiplier: 0x8, + Indirect: 1, + }, + }, + "musl 1.1.24 / Alpine 3.12 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // mov %fs:0x0,%rax + // mov 0x88(%rax),%rax + // mov %edi,%edi + // mov (%rax,%rdi,8),%rax + // ret + 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, + 0x89, 0xff, 0x48, 0x8b, 0x04, 0xf8, 0xc3, + }, + info: TSDInfo{ + Offset: 0x88, + Multiplier: 0x8, + Indirect: 1, + }, + }, + "glibc 2.32 / Fedora 33 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // endbr64 + // cmp $0x1f,%edi + // ja 10bf0 <__pthread_getspecific+0x40> + // lea 0x31(%rdi),%eax + // shl $0x4,%rax # <- 0x31<<4 = 0x310, <<4 = *0x10 + // mov %fs:0x10,%rdx + // add %rdx,%rax + // mov 0x8(%rax),%r8 # <- +8 + // test %r8,%r8 + // je 10beb <__pthread_getspecific+0x3b> + // mov %edi,%edi + // lea 0xc4c2(%rip),%rdx # 1d0a0 <__GI___pthread_keys> + // mov (%rax),%rsi + // shl $0x4,%rdi + // cmp %rsi,(%rdx,%rdi,1) + // jne 10c20 <__pthread_getspecific+0x70> + // mov %r8,%rax + // retq + // code skipped for handling keys >0x1f + 0xf3, 0x0f, 0x1e, 0xfa, 0x83, 0xff, 0x1f, 0x77, + 0x37, 0x8d, 0x47, 0x31, 0x48, 0xc1, 0xe0, 0x04, + 0x64, 0x48, 0x8b, 0x14, 0x25, 0x10, 0x00, 0x00, + 0x00, 0x48, 0x01, 0xd0, 0x4c, 0x8b, 0x40, 0x08, + 0x4d, 0x85, 0xc0, 0x74, 0x16, 0x89, 0xff, 0x48, + 0x8d, 0x15, 0xc2, 0xc4, 0x00, 0x00, 0x48, 0x8b, + 0x30, 0x48, 0xc1, 0xe7, 0x04, 0x48, 0x39, 0x34, + 0x3a, 0x75, 0x35, 0x4c, 0x89, 0xc0, 0xc3, + }, + info: TSDInfo{ + Offset: 0x310 + 8, + Multiplier: 0x10, + }, + }, + "glibc 2.35 / Fedora 36 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // endbr64 + // cmp $0x1f,%edi + // ja 92a40 <__pthread_getspecific@GLIBC_2.2.5+0x40> + // mov %edi,%eax + // add $0x31,%rax + // shl $0x4,%rax + // add %fs:0x10,%rax + // mov 0x8(%rax),%rdx + // test %rdx,%rdx + // je 92a78 <__pthread_getspecific@GLIBC_2.2.5+0x78> + // mov %edi,%edi + // lea 0x167b92(%rip),%rcx + // mov (%rax),%rsi + // shl $0x4,%rdi + // cmp %rsi,(%rcx,%rdi,1) + // jne 92a70 <__pthread_getspecific@GLIBC_2.2.5+0x70> + // mov %rdx,%rax + // ret + // code skipped for handling keys >0x1f + 0xf3, 0x0f, 0x1e, 0xfa, 0x83, 0xff, 0x1f, 0x77, + 0x37, 0x89, 0xf8, 0x48, 0x83, 0xc0, 0x31, 0x48, + 0xc1, 0xe0, 0x04, 0x64, 0x48, 0x03, 0x04, 0x25, + 0x10, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x50, 0x08, + 0x48, 0x85, 0xd2, 0x74, 0x53, 0x89, 0xff, 0x48, + 0x8d, 0x0d, 0x92, 0x7b, 0x16, 0x00, 0x48, 0x8b, + 0x30, 0x48, 0xc1, 0xe7, 0x04, 0x48, 0x39, 0x34, + 0x39, 0x75, 0x35, 0x48, 0x89, 0xd0, 0xc3, + }, + info: TSDInfo{ + Offset: 0x310 + 8, + Multiplier: 0x10, + }, + }, + "booking coredump glibc": { + machine: elf.EM_X86_64, + code: []byte{ + 0x83, 0xff, 0x1f, 0x77, 0x49, 0x89, 0xf8, 0x48, 0x83, 0xc0, 0x30, 0x48, + 0xc1, 0xe0, 0x04, 0x64, 0x48, 0x8b, 0x14, 0x25, 0x10, 0x00, 0x00, 0x00, + 0x48, 0x8d, 0x54, 0x02, 0x10, 0x48, 0x8b, 0x42, 0x08, 0x48, 0x85, 0xc0, + 0x74, 0x1a, 0x89, 0xff, 0x48, 0x8d, 0x0d, 0x61, 0xaa, 0x20, 0x00, 0x48, + 0xc1, 0xe7, 0x04, 0x48, 0x8b, 0x34, 0x39, 0x48, 0x39, 0x32, 0x75, 0x07, + 0xf3, 0xc3, + }, + info: TSDInfo{ + Offset: 0x310 + 8, + Multiplier: 0x10, + }, + }, + } + + for name, test := range testCases { + name := name + test := test + t.Run(name, func(t *testing.T) { + var info TSDInfo + var err error + switch test.machine { + case elf.EM_X86_64: + info, err = ExtractTSDInfoX64_64(test.code) + case elf.EM_AARCH64: + info, err = ExtractTSDInfoARM64(test.code) + } + if errors.Is(err, errArchNotImplemented) { + t.Skip("tests not available on this platform") + } + if assert.NoError(t, err) { + assert.Equal(t, test.info, info, "Wrong TSD info extraction") + } + }) + } +} diff --git a/tpbase/tpbase.go b/tpbase/tpbase.go new file mode 100644 index 00000000..a0f81c02 --- /dev/null +++ b/tpbase/tpbase.go @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// tpbase implements disassembly analysis functions to extract needed data for +// the Thread Pointer Base value handling. Code to analyze several Linux Kernel +// architecture specific functions exist to extract offset of the TPBase value +// relative to the 'struct task_struct'. This is needed to support Thread Local +// Storage access in eBPF. + +package tpbase + +type Analyzer struct { + // FunctionName is the kernel function which can be analyzed + FunctionName string + + // Analyze can inspect the kernel function mentioned above for the Thread Pointer Base + Analyze func([]byte) (uint32, error) +} diff --git a/tracehandler/metrics.go b/tracehandler/metrics.go new file mode 100644 index 00000000..c525705c --- /dev/null +++ b/tracehandler/metrics.go @@ -0,0 +1,35 @@ +/* + * 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 tracehandler + +import "github.com/elastic/otel-profiling-agent/metrics" + +func (m *traceHandler) collectMetrics() { + metrics.AddSlice([]metrics.Metric{ + { + ID: metrics.IDTraceCacheHit, + Value: metrics.MetricValue(m.umTraceCacheHit), + }, + { + ID: metrics.IDTraceCacheMiss, + Value: metrics.MetricValue(m.umTraceCacheMiss), + }, + { + ID: metrics.IDKnownTracesHit, + Value: metrics.MetricValue(m.bpfTraceCacheHit), + }, + { + ID: metrics.IDKnownTracesMiss, + Value: metrics.MetricValue(m.bpfTraceCacheMiss), + }, + }) + + m.umTraceCacheHit = 0 + m.umTraceCacheMiss = 0 + m.bpfTraceCacheHit = 0 + m.bpfTraceCacheMiss = 0 +} diff --git a/tracehandler/tracehandler.go b/tracehandler/tracehandler.go new file mode 100644 index 00000000..fbf63a91 --- /dev/null +++ b/tracehandler/tracehandler.go @@ -0,0 +1,202 @@ +/* + * 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 tracehandler converts raw BPF traces into the enriched user-mode +// format and then forwards them to the reporter. +package tracehandler + +import ( + "context" + "fmt" + "time" + + lru "github.com/elastic/go-freelru" + "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/reporter" + "github.com/elastic/otel-profiling-agent/tracer" + log "github.com/sirupsen/logrus" +) + +// metadataWarnInhibDuration defines the minimum duration between warnings printed +// about failure to obtain metadata for a single PID. +const metadataWarnInhibDuration = 1 * time.Minute + +// 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 { + MonitorInterval() time.Duration +} + +// 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 { + // 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. + ConvertTrace(trace *host.Trace) *libpf.Trace + + // SymbolizationComplete is called after a group of Trace has been symbolized. + // 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) +} + +// 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 { + // Metrics + umTraceCacheHit uint64 + umTraceCacheMiss uint64 + bpfTraceCacheHit uint64 + bpfTraceCacheMiss uint64 + + traceProcessor TraceProcessor + + // bpfTraceCache stores mappings from BPF to user-mode hashes. This allows + // avoiding the overhead of re-doing user-mode symbolization of traces that + // we have recently seen already. + bpfTraceCache *lru.LRU[host.TraceHash, libpf.TraceHash] + + // umTraceCache is a LRU set that suppresses unnecessary resends of traces + // that we have recently reported to the collector already. + umTraceCache *lru.LRU[libpf.TraceHash, libpf.Void] + + // reporter instance to use to send out traces. + reporter reporter.TraceReporter + + // containerMetadataHandler retrieves the metadata associated with the pod or container. + containerMetadataHandler *containermetadata.Handler + + // metadataWarnInhib tracks inhibitions for warnings printed about failure to + // update container metadata (rate-limiting). + metadataWarnInhib *lru.LRU[libpf.PID, libpf.Void] + + times Times +} + +// newTraceHandler creates a new traceHandler +func newTraceHandler(ctx context.Context, rep reporter.TraceReporter, + traceProcessor TraceProcessor, times Times) ( + *traceHandler, error) { + cacheSize := config.TraceCacheEntries() + + bpfTraceCache, err := lru.New[host.TraceHash, libpf.TraceHash]( + cacheSize, func(k host.TraceHash) uint32 { return uint32(k) }) + if err != nil { + return nil, err + } + + umTraceCache, err := lru.New[libpf.TraceHash, libpf.Void]( + cacheSize, libpf.TraceHash.Hash32) + if err != nil { + return nil, err + } + + pidHash := func(x libpf.PID) uint32 { return uint32(x) } + metadataWarnInhib, err := lru.New[libpf.PID, libpf.Void](64, pidHash) + 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, + umTraceCache: umTraceCache, + reporter: rep, + times: times, + containerMetadataHandler: containerMetadataHandler, + metadataWarnInhib: metadataWarnInhib, + } + + return t, nil +} + +func (m *traceHandler) HandleTrace(bpfTrace *host.Trace) { + timestamp := libpf.UnixTime32(libpf.NowAsUInt32()) + defer m.traceProcessor.SymbolizationComplete(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 + } + 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) + m.reporter.ReportCountForTrace(umTrace.Hash, timestamp, 1, + bpfTrace.Comm, meta.PodName, meta.ContainerName) + + // Trace already known to collector by UM hash? + if _, known := m.umTraceCache.Get(umTrace.Hash); known { + m.umTraceCacheHit++ + return + } + m.umTraceCacheMiss++ + + // Nope. Send it now. + m.reporter.ReportFramesForTrace(umTrace) + m.umTraceCache.Add(umTrace.Hash, libpf.Void{}) +} + +// 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, + traceInChan <-chan *host.Trace, times Times, +) error { + handler, err := newTraceHandler(ctx, rep, traceProcessor, times) + if err != nil { + return fmt.Errorf("failed to create traceHandler: %v", err) + } + + go func() { + metricsTicker := time.NewTicker(times.MonitorInterval()) + defer metricsTicker.Stop() + + // Poll the output channels + for { + select { + case traceUpdate := <-traceInChan: + handler.HandleTrace(traceUpdate) + case <-metricsTicker.C: + handler.collectMetrics() + case <-ctx.Done(): + return + } + // Output memory usage in debug builds. + memorydebug.DebugLogMemoryUsage() + } + }() + + return nil +} diff --git a/tracehandler/tracehandler_test.go b/tracehandler/tracehandler_test.go new file mode 100644 index 00000000..4e03cb8e --- /dev/null +++ b/tracehandler/tracehandler_test.go @@ -0,0 +1,179 @@ +/* + * 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 tracehandler + +import ( + "testing" + "time" + + "github.com/elastic/go-freelru" + "github.com/stretchr/testify/require" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/libpf" +) + +type fakeTimes struct { + monitorInterval time.Duration +} + +func defaultTimes() *fakeTimes { + return &fakeTimes{monitorInterval: 1 * time.Hour} +} + +func (ft *fakeTimes) MonitorInterval() time.Duration { return ft.monitorInterval } + +// 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) + +func (f *fakeTraceProcessor) ConvertTrace(trace *host.Trace) *libpf.Trace { + var newTrace libpf.Trace + newTrace.Hash = libpf.NewTraceHash(uint64(trace.Hash), uint64(trace.Hash)) + return &newTrace +} + +func (f *fakeTraceProcessor) SymbolizationComplete(libpf.KTime) { +} + +// 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 +// via the reporter functions (reportCountForTrace / reportFramesForTrace). +type reportedCount struct { + traceHash libpf.TraceHash + count uint16 +} + +type reportedTrace struct { + traceHash libpf.TraceHash +} + +type mockReporter struct { + t *testing.T + reportedCounts []reportedCount + reportedTraces []reportedTrace +} + +func (m *mockReporter) ReportFramesForTrace(trace *libpf.Trace) { + m.reportedTraces = append(m.reportedTraces, reportedTrace{traceHash: trace.Hash}) + m.t.Logf("reportFramesForTrace: new trace 0x%x", trace.Hash) +} + +func (m *mockReporter) ReportCountForTrace(traceHash libpf.TraceHash, + _ libpf.UnixTime32, count uint16, _, _, _ string) { + m.reportedCounts = append(m.reportedCounts, reportedCount{ + traceHash: traceHash, + count: count, + }) + m.t.Logf("reportCountForTrace: 0x%x count: %d", traceHash, count) +} + +func TestTraceHandler(t *testing.T) { + tests := map[string]struct { + input []arguments + expectedCounts []reportedCount + expectedTraces []reportedTrace + expireTimeout time.Duration + }{ + // no input simulates a case where no data is provided as input + // to the functions of traceHandler. + "no input": {input: []arguments{}}, + + // simulates a single trace being received. + "single trace": {input: []arguments{ + {trace: &host.Trace{Hash: host.TraceHash(0x1234)}}, + }, + expectedTraces: []reportedTrace{{traceHash: libpf.NewTraceHash(0x1234, 0x1234)}}, + expectedCounts: []reportedCount{ + {traceHash: libpf.NewTraceHash(0x1234, 0x1234), count: 1}, + }, + }, + + // double trace simulates a case where the same trace is encountered in quick succession. + "double trace": {input: []arguments{ + {trace: &host.Trace{Hash: host.TraceHash(4)}}, + {trace: &host.Trace{Hash: host.TraceHash(4)}}, + }, + expectedTraces: []reportedTrace{{traceHash: libpf.NewTraceHash(4, 4)}}, + expectedCounts: []reportedCount{ + {traceHash: libpf.NewTraceHash(4, 4), count: 1}, + {traceHash: libpf.NewTraceHash(4, 4), count: 1}, + }, + }, + } + + for name, test := range tests { + name := name + test := test + 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(), + } + + for _, input := range test.input { + tuh.HandleTrace(input.trace) + time.Sleep(input.delay) + } + + 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)) + } + + 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 + for _, repCount := range r.reportedCounts { + if expCount == repCount { + found = true + break + } + } + if !found { + t.Fatalf("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 new file mode 100644 index 00000000..5fc287d0 --- /dev/null +++ b/tracer/ebpf_integration_test.go @@ -0,0 +1,11 @@ +//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 tracer + +// diff --git a/tracer/events.go b/tracer/events.go new file mode 100644 index 00000000..a656afc5 --- /dev/null +++ b/tracer/events.go @@ -0,0 +1,217 @@ +/* + * 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 tracer + +import ( + "context" + "errors" + "os" + "sync/atomic" + "time" + "unsafe" + + "github.com/cilium/ebpf" + "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/support" +) + +/* +#include +#include "../support/ebpf/types.h" +*/ +import "C" + +const ( + // Length of the pidEvents channel. It must be large enough so the + // consuming goroutine doesn't go idle due to scheduling, but small enough + // so that the hostagent startup phase can wait on most PID notifications + // to be processed before starting the tracer. + pidEventBufferSize = 10 +) + +// StartPIDEventProcessor spawns a goroutine to process PID events. +func (t *Tracer) StartPIDEventProcessor(ctx context.Context) error { + go t.processPIDEvents(ctx) + return t.populatePIDs(ctx) +} + +// Process the PID events that are incoming in the Tracer channel. +func (t *Tracer) processPIDEvents(ctx context.Context) { + pidCleanupTicker := time.NewTicker(t.intervals.PIDCleanupInterval()) + defer pidCleanupTicker.Stop() + for { + select { + case pid := <-t.pidEvents: + t.processManager.SynchronizeProcess(process.New(pid)) + case <-pidCleanupTicker.C: + t.processManager.CleanupPIDs() + case <-ctx.Done(): + return + } + } +} + +// handleGenericPID triggers immediate processing of eBPF-reported PIDs. +// WARNING: Not executed as a goroutine: needs to stay lightweight, and nonblocking. +func (t *Tracer) handleGenericPID() { + // Non-blocking trigger sending. If the attempt would block + // some other goroutine is already sending this notification. + select { + case t.triggerPIDProcessing <- true: + default: + } +} + +// triggerPidEvent is a trigger function for the eBPF map report_events. It is +// called for every event that is received in user space from this map. The underlying +// C structure in the received data is transformed to a Go structure and the event +// handler is invoked. +func (t *Tracer) triggerPidEvent(data []byte) { + event := (*C.Event)(unsafe.Pointer(&data[0])) + if event.event_type == support.EventTypeGenericPID { + t.handleGenericPID() + } +} + +// startPerfEventMonitor spawns a goroutine that receives events from the given +// perf event map by waiting for events the kernel. Every event in the buffer +// will wake up user-land. +// +// For each received event, triggerFunc is called. triggerFunc may NOT store +// references into the buffer that it is given: the buffer is re-used across +// calls. Returns a function that can be called to retrieve perf event array +// error counts. +func startPerfEventMonitor(ctx context.Context, perfEventMap *ebpf.Map, + triggerFunc func([]byte), perCPUBufferSize int) func() (lost, noData, readError uint64) { + eventReader, err := perf.NewReader(perfEventMap, perCPUBufferSize) + if err != nil { + log.Fatalf("Failed to setup perf reporting via %s: %v", perfEventMap, err) + } + + var lostEventsCount, readErrorCount, noDataCount atomic.Uint64 + go func() { + var data perf.Record + for { + select { + case <-ctx.Done(): + return + default: + if err := eventReader.ReadInto(&data); err != nil { + readErrorCount.Add(1) + continue + } + if data.LostSamples != 0 { + lostEventsCount.Add(data.LostSamples) + continue + } + if len(data.RawSample) == 0 { + noDataCount.Add(1) + continue + } + triggerFunc(data.RawSample) + } + } + }() + + return func() (lost, noData, readError uint64) { + lost = lostEventsCount.Swap(0) + noData = noDataCount.Swap(0) + readError = readErrorCount.Swap(0) + return + } +} + +// startPollingPerfEventMonitor spawns a goroutine that receives events from +// the given perf event map by periodically polling the perf event buffer. +// Events written to the perf event buffer do not wake user-land immediately. +// +// For each received event, triggerFunc is called. triggerFunc may NOT store +// references into the buffer that it is given: the buffer is re-used across +// calls. Returns a function that can be called to retrieve perf event array +// error counts. +func startPollingPerfEventMonitor(ctx context.Context, perfEventMap *ebpf.Map, + pollFrequency time.Duration, perCPUBufferSize int, triggerFunc func([]byte), +) func() (lost, noData, readError uint64) { + eventReader, err := perf.NewReader(perfEventMap, perCPUBufferSize) + if err != nil { + log.Fatalf("Failed to setup perf reporting via %s: %v", perfEventMap, err) + } + + // A deadline of zero is treated as "no deadline". A deadline in the past + // means "always return immediately". We thus set a deadline 1 second after + // unix epoch to always ensure the latter behavior. + eventReader.SetDeadline(time.Unix(1, 0)) + + pollTicker := time.NewTicker(pollFrequency) + + var lostEventsCount, readErrorCount, noDataCount atomic.Uint64 + go func() { + var data perf.Record + + PollLoop: + for { + select { + case <-pollTicker.C: + // Continue execution below. + case <-ctx.Done(): + break PollLoop + } + + // Eagerly read events until the buffer is exhausted. + for { + if err = eventReader.ReadInto(&data); err != nil { + if !errors.Is(err, os.ErrDeadlineExceeded) { + readErrorCount.Add(1) + } + + break + } + if data.LostSamples != 0 { + lostEventsCount.Add(data.LostSamples) + continue + } + if len(data.RawSample) == 0 { + noDataCount.Add(1) + continue + } + triggerFunc(data.RawSample) + } + } + }() + + return func() (lost, noData, readError uint64) { + lost = lostEventsCount.Swap(0) + noData = noDataCount.Swap(0) + readError = readErrorCount.Swap(0) + return + } +} + +// startEventMonitor spawns a goroutine that receives events from the +// map report_events. Returns a function that can be called to retrieve +// perf event array metrics. +func (t *Tracer) startEventMonitor(ctx context.Context) func() []metrics.Metric { + eventMap, ok := t.ebpfMaps["report_events"] + if !ok { + log.Fatalf("Map report_events is not available") + } + + getPerfErrorCounts := startPerfEventMonitor(ctx, eventMap, t.triggerPidEvent, os.Getpagesize()) + return func() []metrics.Metric { + lost, noData, readError := getPerfErrorCounts() + + return []metrics.Metric{ + {ID: metrics.IDPerfEventLost, Value: metrics.MetricValue(lost)}, + {ID: metrics.IDPerfEventNoData, Value: metrics.MetricValue(noData)}, + {ID: metrics.IDPerfEventReadError, Value: metrics.MetricValue(readError)}, + } + } +} diff --git a/tracer/helper.go b/tracer/helper.go new file mode 100644 index 00000000..2835c980 --- /dev/null +++ b/tracer/helper.go @@ -0,0 +1,44 @@ +/* + * 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 tracer + +// hasProbeReadBug returns true if the given Linux kernel version is affected by +// a bug that can lead to system freezes. +func hasProbeReadBug(major, minor, patch uint32) bool { + if major == 5 && minor >= 19 { + return true + } else if major == 6 { + switch minor { + case 0, 2: + return true + case 1: + // The bug fix was backported to the LTS kernel 6.1.36 with + // nolint:lll + // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/mm/maccess.c?h=v6.1.36&id=2e7ad879e1b0256fb9e4703fd6cd2864d707dea7 + if patch < 36 { + return true + } + return false + case 3: + // The bug fix was backported to the LTS kernel 6.3.10 with + // nolint:lll + // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/mm/maccess.c?h=v6.3.10&id=3acb3dd3145b54933e88ae107e1288c1147d6d33 + if patch < 10 { + return true + } + return false + default: + // The bug fix landed in 6.4 with + // nolint:lll + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/mm/maccess.c?h=v6.4&id=d319f344561de23e810515d109c7278919bff7b0 + // So newer versions of the Linux kernel are not affected. + return false + } + } + // Other Linux kernel versions, like 4.x, are not affected by this bug. + return false +} diff --git a/tracer/maccess.go b/tracer/maccess.go new file mode 100644 index 00000000..27db17af --- /dev/null +++ b/tracer/maccess.go @@ -0,0 +1,64 @@ +/* + * 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 tracer + +import ( + "runtime" + + cebpf "github.com/cilium/ebpf" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/maccess" + log "github.com/sirupsen/logrus" +) + +// checkForMmaccessPatch validates if a Linux kernel function is patched by +// extracting the kernel code of the function and analyzing it. +func checkForMaccessPatch(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, + kernelSymbols *libpf.SymbolMap) bool { + faultyFunc, err := kernelSymbols.LookupSymbol( + libpf.SymbolName("copy_from_user_nofault")) + if err != nil { + log.Warnf("Failed to look up Linux kernel symbol "+ + "'copy_from_user_nofault': %v", err) + return false + } + + code, err := loadKernelCode(coll, maps, faultyFunc.Address) + if err != nil { + log.Warnf("Failed to load code for %s: %v", faultyFunc.Name, err) + return false + } + + newCheckFunc, err := kernelSymbols.LookupSymbol( + libpf.SymbolName("nmi_uaccess_okay")) + if err != nil { + if runtime.GOARCH == "arm64" { + // On arm64 this symbol might not be available and we do not use + // the symbol address in the arm64 case to check for the patch. + // As there was an error getting the symbol, newCheckFunc is nil. + // To still be able to access newCheckFunc safely, create a dummy element. + newCheckFunc = &libpf.Symbol{ + Address: 0, + } + } else { + log.Warnf("Failed to look up Linux kernel symbol 'nmi_uaccess_okay': %v", + err) + + // Without the symbol information, we can not continue with checking the + // function and determine whether it got patched. + return false + } + } + + patched, err := maccess.CopyFromUserNoFaultIsPatched(code, uint64(faultyFunc.Address), + uint64(newCheckFunc.Address)) + if err != nil { + log.Warnf("Failed to check if %s is patched: %v", faultyFunc.Name, err) + return false + } + return patched +} diff --git a/tracer/probe_linux.go b/tracer/probe_linux.go new file mode 100644 index 00000000..4a660e8e --- /dev/null +++ b/tracer/probe_linux.go @@ -0,0 +1,137 @@ +//go:build linux +// +build 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 tracer + +import ( + "bytes" + "fmt" + "os" + "strings" + + "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" +) + +// ProbeBPFSyscall checks if the syscall EBPF is available on the system. +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 nil +} + +// getTracepointID returns the system specific tracepoint ID for a given tracepoint. +func getTracepointID(tracepoint string) (uint64, error) { + id, err := os.ReadFile("/sys/kernel/debug/tracing/events/syscalls/" + tracepoint + "/id") + if err != nil { + return 0, fmt.Errorf("failed to read tracepoint ID for %s: %v", tracepoint, err) + } + tid := libpf.DecToUint64(strings.TrimSpace(string(id))) + return tid, nil +} + +// GetCurrentKernelVersion returns the major, minor and patch version of the kernel of the host +// from the utsname struct. +func GetCurrentKernelVersion() (major, minor, patch uint32, err error) { + var uname unix.Utsname + if err := unix.Uname(&uname); err != nil { + return 0, 0, 0, fmt.Errorf("could not get Kernel Version: %v", err) + } + fmt.Fscanf(bytes.NewReader(uname.Release[:]), "%d.%d.%d", &major, &minor, &patch) + return major, minor, patch, nil +} + +// ProbeTracepoint checks if tracepoints are available on the system, so we can attach +// our eBPF code there. +func ProbeTracepoint() error { + ins := asm.Instructions{ + // set exit code to 0 + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + } + + // The check of the kernel version was removed with + // commit 6c4fc209fcf9d27efbaa48368773e4d2bfbd59aa. So kernel < 4.20 + // need to set the kernel version to not be rejected by the verifier. + major, minor, patch, err := GetCurrentKernelVersion() + if err != nil { + return err + } + kernelVersion := libpf.VersionUint(major, minor, patch) + restoreRlimit, err := rlimit.MaximizeMemlock() + if err != nil { + return fmt.Errorf("failed to increase rlimit: %v", err) + } + defer restoreRlimit() + + prog, err := cebpf.NewProgram(&cebpf.ProgramSpec{ + Type: cebpf.TracePoint, + License: "GPL", + Instructions: ins, + KernelVersion: kernelVersion, + }) + if err != nil { + return fmt.Errorf("failed to create tracepoint_probe: %v", err) + } + defer prog.Close() + + var tid uint64 + // sys_enter_mmap is the first tracepoint we have used + tid, err = getTracepointID("sys_enter_mmap") + if err != nil { + return fmt.Errorf("failed to get id for tracepoint: %v", err) + } + + attr := unix.PerfEventAttr{ + Type: unix.PERF_TYPE_TRACEPOINT, + Config: tid, + Sample_type: unix.PERF_SAMPLE_RAW, + Sample: 1, + Wakeup: 1, + } + + pfd, err := unix.PerfEventOpen(&attr, -1, 0, -1, unix.PERF_FLAG_FD_CLOEXEC) + if err != nil { + return fmt.Errorf("unable to open perf events: %v", err) + } + defer func() { + if err = unix.Close(pfd); err != nil { + log.Fatalf("Failed to close tracepoint sys_enter_mmap probe: %v", err) + } + }() + + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(pfd), + unix.PERF_EVENT_IOC_ENABLE, 0); errno != 0 { + return fmt.Errorf("unable to set up perf events: %d", errno) + } + + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(pfd), + unix.PERF_EVENT_IOC_SET_BPF, uintptr(prog.FD())); errno != 0 { + return fmt.Errorf("unable to attach bpf program to perf event %d: %d", tid, errno) + } + + // The test was successful, so disable the tracepoint and clean up. + // In kernel < 4.15 we can not attach multiple eBPF programs to the same tracepoint. + // This was changed in the kernel with commit e87c6bc3852b981e71c757be20771546ce9f76f3. + // So it is important not only to disable the tracepoint but also close its + // perf event file descriptor. + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(pfd), + unix.PERF_EVENT_IOC_DISABLE, 0); errno != 0 { + return fmt.Errorf("unable to disable perf events: %v", err) + } + return nil +} diff --git a/tracer/probe_other.go b/tracer/probe_other.go new file mode 100644 index 00000000..429ffce6 --- /dev/null +++ b/tracer/probe_other.go @@ -0,0 +1,25 @@ +//go:build !linux +// +build !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 tracer + +import ( + "fmt" + "runtime" +) + +// ProbeBPFSyscall checks if the syscall EBPF is available on the system. +func ProbeBPFSyscall() error { + return fmt.Errorf("eBPF is not available on your system %s", runtime.GOOS) +} + +// ProbeTracepoint checks if tracepoints are available on the system. +func ProbeTracepoint() error { + return fmt.Errorf("tracepoints are not available on your system %s", runtime.GOOS) +} diff --git a/tracer/systemconfig.go b/tracer/systemconfig.go new file mode 100644 index 00000000..cf9a36d0 --- /dev/null +++ b/tracer/systemconfig.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 tracer + +// #include "../support/ebpf/types.h" +import "C" + +import ( + "unsafe" + + "github.com/elastic/otel-profiling-agent/config" + + cebpf "github.com/cilium/ebpf" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/pacmask" + log "github.com/sirupsen/logrus" +) + +func loadSystemConfig(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, + kernelSymbols *libpf.SymbolMap, includeTracers []bool) error { + pacMask := pacmask.GetPACMask() + + if pacMask != uint64(0) { + log.Infof("Determined PAC mask to be 0x%016X", pacMask) + } else { + log.Debug("PAC is not enabled on the system.") + } + + // In eBPF, we need the mask to AND off the PAC bits, so we invert it. + invPacMask := ^pacMask + + var tpbaseOffset uint64 + if includeTracers[config.PerlTracer] || includeTracers[config.PythonTracer] { + var err error + tpbaseOffset, err = loadTPBaseOffset(coll, maps, kernelSymbols) + if 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), + } + + key0 := uint32(0) + return maps["system_config"].Update(unsafe.Pointer(&key0), unsafe.Pointer(&cfg), + cebpf.UpdateAny) +} diff --git a/tracer/tpbase.go b/tracer/tpbase.go new file mode 100644 index 00000000..cac94dd1 --- /dev/null +++ b/tracer/tpbase.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 tracer + +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" +) + +// This file contains code to extract the offset of the thread pointer base variable in +// the `task_struct` kernel struct, which is needed by e.g. Python and Perl tracers. +// This offset varies depending on kernel configuration, so we have to learn it dynamically +// at run time. +// +// Unfortunately, /dev/kmem is often disabled for security reasons, so a BPF helper is used to +// read the kernel memory in portable manner. This code is then analyzed to get the data. +// +// If you're wondering how to check the disassembly of a kernel function: +// 1) Extract your vmlinuz image (the extract-vmlinux script is in the Linux kernel source tree) +// linux/scripts/extract-vmlinux /boot/vmlinuz-5.6.11 > kernel.elf +// 2) Find the address of aout_dump_debugregs in the ELF +// address=$(cat /boot/System.map-5.6.11 | grep "T aout_dump_debugregs" | awk '{print $1}') +// 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. +func loadTPBaseOffset(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, + kernelSymbols *libpf.SymbolMap) (uint64, error) { + var tpbaseOffset uint32 + for _, analyzer := range tpbase.GetAnalyzers() { + sym, err := kernelSymbols.LookupSymbol(libpf.SymbolName(analyzer.FunctionName)) + if err != nil { + continue + } + + code, err := loadKernelCode(coll, maps, sym.Address) + if err != nil { + return 0, err + } + + tpbaseOffset, err = analyzer.Analyze(code) + if err != nil { + return 0, fmt.Errorf("%w: %s", err, hex.Dump(code)) + } + log.Infof("Found tpbase offset: %v (via %s)", tpbaseOffset, analyzer.FunctionName) + break + } + + if tpbaseOffset == 0 { + return 0, errors.New("no supported symbol found") + } + + // Sanity-check against reasonable values. We expect something in the ~2000-10000 range, + // but allow for some additional slack on top of that. + if tpbaseOffset < 500 || tpbaseOffset > 20000 { + return 0, fmt.Errorf("tpbase offset %v doesn't look sane", tpbaseOffset) + } + + return uint64(tpbaseOffset), nil +} diff --git a/tracer/tracepoints.go b/tracer/tracepoints.go new file mode 100644 index 00000000..bc9520e6 --- /dev/null +++ b/tracer/tracepoints.go @@ -0,0 +1,45 @@ +/* + * 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 tracer + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + + "github.com/elastic/otel-profiling-agent/libpf/rlimit" +) + +// attachToTracepoint attaches an eBPF program of type tracepoint to a tracepoint in the kernel +// defined by group and name. +// Otherwise it returns an error. +func (t *Tracer) attachToTracepoint(group, name string, prog *ebpf.Program) error { + hp := hookPoint{ + group: group, + name: name, + } + hook, err := link.Tracepoint(hp.group, hp.name, prog, nil) + if err != nil { + return fmt.Errorf("failed to configure tracepoint on %#v: %v", hp, err) + } + t.hooks[hp] = hook + return nil +} + +// AttachSchedMonitor attaches a kprobe to the process scheduler. This hook detects the +// exit of a process and enables us to clean up data we associated with this process. +func (t *Tracer) AttachSchedMonitor() error { + restoreRlimit, err := rlimit.MaximizeMemlock() + if err != nil { + return fmt.Errorf("failed to adjust rlimit: %v", err) + } + defer restoreRlimit() + + prog := t.ebpfProgs["tracepoint__sched_process_exit"] + return t.attachToTracepoint("sched", "sched_process_exit", prog) +} diff --git a/tracer/tracer.go b/tracer/tracer.go new file mode 100644 index 00000000..9df11a9a --- /dev/null +++ b/tracer/tracer.go @@ -0,0 +1,1133 @@ +/* + * 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 tracer contains functionality for populating tracers. +package tracer + +import ( + "bufio" + "context" + "errors" + "fmt" + "math/rand" + "strings" + "sync/atomic" + "time" + "unsafe" + + cebpf "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + lru "github.com/elastic/go-freelru" + "github.com/elastic/go-perf" + log "github.com/sirupsen/logrus" + "github.com/zeebo/xxh3" + + "github.com/elastic/otel-profiling-agent/config" + "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/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/support" +) + +/* +#include +#include "../support/ebpf/types.h" +*/ +import "C" + +// Compile time check to make sure config.Times satisfies the interfaces. +var _ Intervals = (*config.Times)(nil) + +const ( + // ProbabilisticThresholdMax defines the upper bound of the probabilistic profiling + // threshold. + ProbabilisticThresholdMax = 100 +) + +// Constants that define the status of probabilistic profiling. +const ( + probProfilingEnable = 1 + probProfilingDisable = -1 +) + +// Intervals is a subset of config.IntervalsAndTimers. +type Intervals interface { + MonitorInterval() time.Duration + TracePollInterval() time.Duration + PIDCleanupInterval() time.Duration +} + +// Tracer provides an interface for loading and initializing the eBPF components as +// well as for monitoring the output maps for new traces and count updates. +type Tracer struct { + fallbackSymbolHit atomic.Uint64 + fallbackSymbolMiss atomic.Uint64 + + // ebpfMaps holds the currently loaded eBPF maps. + ebpfMaps map[string]*cebpf.Map + // ebpfProgs holds the currently loaded eBPF programs. + ebpfProgs map[string]*cebpf.Program + + // kernelSymbols is used to hold the kernel symbol addresses we are tracking + kernelSymbols *libpf.SymbolMap + + // kernelModules holds symbols/addresses for the kernel module address space + kernelModules *libpf.SymbolMap + + // perfEntrypoints holds a list of frequency based perf events that are opened on the system. + perfEntrypoints xsync.RWMutex[[]*perf.Event] + + // hooks holds references to loaded eBPF hooks. + hooks map[hookPoint]link.Link + + // processManager keeps track of loading, unloading and organization of information + // that is required to unwind processes in the kernel. This includes maintaining the + // associated eBPF maps. + processManager *pm.ProcessManager + + // transmittedFallbackSymbols keeps track of the already-transmitted fallback symbols. + // It is not thread-safe: concurrent accesses must be synchronized. + transmittedFallbackSymbols *lru.LRU[libpf.FrameID, libpf.Void] + + // triggerPIDProcessing is used as manual trigger channel to request immediate + // processing of pending PIDs. This is requested on notifications from eBPF code + // when process events take place (new, exit, unknown PC). + triggerPIDProcessing chan bool + + // 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 + + // intervals provides access to globally configured timers and counters. + intervals Intervals + + hasBatchOperations bool + + // moduleFileIDs maps kernel module names to their respective FileID. + moduleFileIDs map[string]libpf.FileID + + // reporter allows swapping out the reporter implementation. + reporter reporter.SymbolReporter +} + +// hookPoint specifies the group and name of the hooked point in the kernel. +type hookPoint struct { + group, name string +} + +// 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) { + result := make(map[string]libpf.FileID, kernelModules.Len()) + kernelModules.ScanAllNames(func(name libpf.SymbolName) { + nameStr := string(name) + if !libpf.IsValidString(nameStr) { + log.Errorf("Invalid string representation of file name in "+ + "processKernelModulesMetadata: %v", []byte(nameStr)) + return + } + + // Read the kernel and modules ELF notes from sysfs (works since Linux 2.6.24) + notesFile := fmt.Sprintf("/sys/module/%s/notes/.note.gnu.build-id", nameStr) + + // The vmlinux notes section is in a different location + if nameStr == "vmlinux" { + notesFile = "/sys/kernel/notes" + } + + buildID, err := pfelf.GetBuildIDFromNotesFile(notesFile) + var fileID libpf.FileID + // Require at least 16 bytes of BuildID to ensure there is enough entropy for a FileID. + // 16 bytes could happen when --build-id=md5 is passed to `ld`. This would imply a custom + // kernel. + if err == nil && len(buildID) >= 16 { + fileID = pfelf.CalculateKernelFileID(buildID) + result[nameStr] = fileID + rep.ExecutableMetadata(ctx, fileID, nameStr, buildID) + } else { + log.Errorf("Failed to get GNU BuildID for kernel module %s: '%s' (%v)", + nameStr, buildID, err) + } + }) + + 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 + } + 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), + }, + }) + }) +} + +// 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) { + 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) + 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) + if err != nil { + return nil, fmt.Errorf("failed to load eBPF maps: %v", err) + } + + hasBatchOperations := ebpfHandler.SupportsGenericBatchOperations() + + processManager, err := pm.New(ctx, includeTracers, intervals.MonitorInterval(), ebpfHandler, + nil, rep, localStackDeltaProvider, filterErrorFrames) + if err != nil { + return nil, fmt.Errorf("failed to create processManager: %v", err) + } + + const fallbackSymbolsCacheSize = 16384 + + kernelModules, err := proc.GetKernelModules("/proc/modules", kernelSymbols) + if err != nil { + return nil, fmt.Errorf("failed to read kernel modules: %v", err) + } + + transmittedFallbackSymbols, err := + lru.New[libpf.FrameID, libpf.Void](fallbackSymbolsCacheSize, libpf.FrameID.Hash32) + if err != nil { + return nil, fmt.Errorf("unable to instantiate transmitted fallback symbols cache: %v", err) + } + + moduleFileIDs, err := processKernelModulesMetadata(ctx, rep, kernelModules) + if err != nil { + return nil, fmt.Errorf("failed to extract kernel modules metadata: %v", err) + } + + perfEventList := []*perf.Event{} + + return &Tracer{ + processManager: processManager, + kernelSymbols: kernelSymbols, + kernelModules: kernelModules, + transmittedFallbackSymbols: transmittedFallbackSymbols, + triggerPIDProcessing: make(chan bool, 1), + pidEvents: make(chan libpf.PID, pidEventBufferSize), + ebpfMaps: ebpfMaps, + ebpfProgs: ebpfProgs, + hooks: make(map[hookPoint]link.Link), + intervals: intervals, + hasBatchOperations: hasBatchOperations, + perfEntrypoints: xsync.NewRWMutex(perfEventList), + moduleFileIDs: moduleFileIDs, + reporter: rep, + }, nil +} + +// Close provides functionality for Tracer to perform cleanup tasks. +// NOTE: Close may be called multiple times in succession. +func (t *Tracer) Close() { + events := t.perfEntrypoints.WLock() + for _, event := range *events { + if err := event.Disable(); err != nil { + log.Errorf("Failed to disable perf event: %v", err) + } + if err := event.Close(); err != nil { + log.Errorf("Failed to close perf event: %v", err) + } + } + *events = nil + t.perfEntrypoints.WUnlock(&events) + + // Avoid resource leakage by closing all kernel hooks. + for hookPoint, hook := range t.hooks { + if err := hook.Close(); err != nil { + log.Errorf("Failed to close '%s/%s': %v", hookPoint.group, hookPoint.name, err) + } + delete(t.hooks, hookPoint) + } + + t.processManager.Close() +} + +func buildStackDeltaTemplates(coll *cebpf.CollectionSpec) error { + // Prepare the inner map template of the stack deltas map-of-maps. + // This cannot be provided from the eBPF C code, and needs to be done here. + for i := support.StackDeltaBucketSmallest; i <= support.StackDeltaBucketLargest; i++ { + mapName := fmt.Sprintf("exe_id_to_%d_stack_deltas", i) + def := coll.Maps[mapName] + if def == nil { + return fmt.Errorf("ebpf map '%s' not found", mapName) + } + def.InnerMap = &cebpf.MapSpec{ + Type: cebpf.Array, + KeySize: uint32(C.sizeof_uint32_t), + ValueSize: uint32(C.sizeof_StackDelta), + MaxEntries: 1 << i, + } + } + return nil +} + +// 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) { + // 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. + // References to eBPF maps in the eBPF programs are just placeholders that need to be + // replaced by the actual loaded maps later on with RewriteMaps before loading the + // programs into the kernel. + coll, err := support.LoadCollectionSpec() + if err != nil { + return nil, nil, fmt.Errorf("failed to load specification for tracers: %v", err) + } + + err = buildStackDeltaTemplates(coll) + if err != nil { + return nil, nil, err + } + + ebpfMaps = make(map[string]*cebpf.Map) + ebpfProgs = make(map[string]*cebpf.Program) + + // Load all maps into the kernel that are used later on in eBPF programs. So we can rewrite + // in the next step the placesholders in the eBPF programs with the file descriptors of the + // loaded maps in the kernel. + if err = loadAllMaps(coll, ebpfMaps); err != nil { + return nil, nil, fmt.Errorf("failed to load eBPF maps: %v", err) + } + + // Replace the place holders for map access in the eBPF programs with + // the file descriptors of the loaded maps. + // nolint:staticcheck + if err = coll.RewriteMaps(ebpfMaps); err != nil { + return nil, nil, fmt.Errorf("failed to rewrite maps: %v", err) + } + + if !config.NoKernelVersionCheck() { + var major, minor, patch uint32 + major, minor, patch, err = GetCurrentKernelVersion() + if err != nil { + return nil, nil, fmt.Errorf("failed to get kernel version: %v", err) + } + if hasProbeReadBug(major, minor, patch) { + patched := checkForMaccessPatch(coll, ebpfMaps, kernelSymbols) + if !patched { + return nil, nil, fmt.Errorf("your kernel version %d.%d.%d is affected by a Linux "+ + "kernel bug that can lead to system freezes, terminating host "+ + "agent now to avoid triggering this bug", major, minor, patch) + } + } + } + + 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 { + return nil, nil, fmt.Errorf("failed to load system config: %v", err) + } + + if err = removeTemporaryMaps(ebpfMaps); err != nil { + return nil, nil, fmt.Errorf("failed to remove temporary maps: %v", err) + } + + return ebpfMaps, ebpfProgs, nil +} + +// 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) + } + delete(ebpfMaps, "codedump_code") + return nil +} + +// loadAllMaps loads all eBPF maps that are used in our eBPF programs. +func loadAllMaps(coll *cebpf.CollectionSpec, ebpfMaps map[string]*cebpf.Map) error { + restoreRlimit, err := rlimit.MaximizeMemlock() + if err != nil { + return fmt.Errorf("failed to adjust rlimit: %v", err) + } + defer restoreRlimit() + + // Redefine the maximum number of map entries for selected eBPF maps. + adaption := make(map[string]uint32, 4) + + const ( + // The following sizes X are used as 2^X, and determined empirically + + // 1 million executable pages / 4GB of executable address space + pidPageMappingInfoSize = 20 + + stackDeltaPageToInfoSize = 16 + exeIDToStackDeltasSize = 16 + ) + + adaption["pid_page_to_mapping_info"] = + 1 << uint32(pidPageMappingInfoSize+config.MapScaleFactor()) + adaption["stack_delta_page_to_info"] = + 1 << uint32(stackDeltaPageToInfoSize+config.MapScaleFactor()) + + for i := support.StackDeltaBucketSmallest; i <= support.StackDeltaBucketLargest; i++ { + mapName := fmt.Sprintf("exe_id_to_%d_stack_deltas", i) + adaption[mapName] = 1 << uint32(exeIDToStackDeltasSize+config.MapScaleFactor()) + } + + for mapName, mapSpec := range coll.Maps { + if newSize, ok := adaption[mapName]; ok { + log.Debugf("Size of eBPF map %s: %v", mapName, newSize) + mapSpec.MaxEntries = newSize + } + ebpfMap, err := cebpf.NewMap(mapSpec) + if err != nil { + return fmt.Errorf("failed to load %s: %v", mapName, err) + } + ebpfMaps[mapName] = ebpfMap + } + + 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 { + restoreRlimit, err := rlimit.MaximizeMemlock() + if err != nil { + return fmt.Errorf("failed to adjust rlimit: %v", err) + } + 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 + // name of the eBPF program + name string + // progID defines the ID for the eBPF program that is used as key in the tailcallMap. + progID uint32 + // noTailCallTarget indicates if this eBPF program should be added to the tailcallMap. + noTailCallTarget bool + } + + logLevel, logSize := config.BpfVerifierLogSetting() + programOptions := cebpf.ProgramOptions{ + LogLevel: cebpf.LogLevel(logLevel), + LogSize: logSize, + } + + for _, unwindProg := range []prog{ + { + progID: uint32(support.ProgUnwindStop), + name: "unwind_stop", + }, + { + progID: uint32(support.ProgUnwindNative), + name: "unwind_native", + }, + { + progID: uint32(support.ProgUnwindHotspot), + name: "unwind_hotspot", + enable: []config.TracerType{config.HotspotTracer}, + }, + { + progID: uint32(support.ProgUnwindPerl), + name: "unwind_perl", + enable: []config.TracerType{config.PerlTracer}, + }, + { + progID: uint32(support.ProgUnwindPHP), + name: "unwind_php", + enable: []config.TracerType{config.PHPTracer}, + }, + { + progID: uint32(support.ProgUnwindPython), + name: "unwind_python", + enable: []config.TracerType{config.PythonTracer}, + }, + { + progID: uint32(support.ProgUnwindRuby), + name: "unwind_ruby", + enable: []config.TracerType{config.RubyTracer}, + }, + { + progID: uint32(support.ProgUnwindV8), + name: "unwind_v8", + enable: []config.TracerType{config.V8Tracer}, + }, + { + name: "tracepoint__sched_process_exit", + noTailCallTarget: true, + }, + { + name: "native_tracer_entry", + noTailCallTarget: true, + }, + } { + if len(unwindProg.enable) > 0 && !isProgramEnabled(includeTracers, unwindProg.enable) { + continue + } + + // Load the eBPF program into the kernel. If no error is returned, + // the eBPF program can be used/called/triggered from now on. + unwinder, err := cebpf.NewProgramWithOptions(coll.Programs[unwindProg.name], + programOptions) + if err != nil { + // These errors tend to have hundreds of lines, so we print each line individually. + scanner := bufio.NewScanner(strings.NewReader(err.Error())) + for scanner.Scan() { + log.Error(scanner.Text()) + } + return fmt.Errorf("failed to load %s", unwindProg.name) + } + + ebpfProgs[unwindProg.name] = unwinder + fd := uint32(unwinder.FD()) + if unwindProg.noTailCallTarget { + continue + } + if err := tailcallMap.Update(unsafe.Pointer(&unwindProg.progID), unsafe.Pointer(&fd), + cebpf.UpdateAny); err != nil { + // Every eBPF program that is loaded within loadUnwinders can be the + // destination of a tail call of another eBPF program. If we can not update + // the eBPF map that manages these destinations our unwinding will fail. + return fmt.Errorf("failed to update tailcall map: %v", err) + } + } + + return nil +} + +// List PIDs in /proc and send them in the Tracer channel for reading. +func (t *Tracer) populatePIDs(ctx context.Context) error { + // Inform the process manager and our backend about the new mappings. + pids, err := proc.ListPIDs() + if err != nil { + return fmt.Errorf("failure reading PID list from /proc: %v", err) + } + for _, pid := range pids { + for { + select { + case <-ctx.Done(): + return nil + case t.pidEvents <- pid: + goto next_pid + default: + // Workaround to implement a non blocking send to a channel. + // To avoid a busy loop on this non blocking channel send operation + // time.Sleep() is used. + time.Sleep(50 * time.Millisecond) + } + } + next_pid: + } + return nil +} + +// insertKernelFrames fetches the kernel stack frames for a particular kstackID and populates +// the trace with these kernel frames. It also allocates the memory for the frames of the trace. +// It returns the number of kernel frames for kstackID or an error. +func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, + kstackID int32) (uint32, error) { + cKstackID := C.s32(kstackID) + kstackVal := make([]C.uint64_t, support.PerfMaxStackDepth) + + if err := t.ebpfMaps["kernel_stackmap"].Lookup(unsafe.Pointer(&cKstackID), + unsafe.Pointer(&kstackVal[0])); err != nil { + return 0, fmt.Errorf("failed to lookup kernel frames for stackID %d: %v", kstackID, err) + } + + // The kernel returns absolute addresses in kernel address + // space format. Here just the stack length is needed. + // But also debug print the symbolization based on kallsyms. + var kstackLen uint32 + for kstackLen < support.PerfMaxStackDepth && kstackVal[kstackLen] != 0 { + kstackLen++ + } + + trace.Frames = make([]host.Frame, kstackLen+ustackLen) + + var kernelSymbolCacheHit, kernelSymbolCacheMiss uint64 + + for i := uint32(0); i < kstackLen; i++ { + var fileID libpf.FileID + // Translate the kernel address into something that can be + // later symbolized. The address is made relative to + // matching module's ELF .text section: + // - main image should have .text section at start of the code segment + // - modules are ELF object files (.o) without program headers and + // LOAD segments. the address is relative to the .text section + mod, addr, _ := t.kernelModules.LookupByAddress( + libpf.SymbolValue(kstackVal[i])) + symbol, offs, foundSymbol := t.kernelSymbols.LookupByAddress( + libpf.SymbolValue(kstackVal[i])) + + fileID, foundFileID := t.moduleFileIDs[string(mod)] + + if !foundFileID { + fileID = libpf.UnknownKernelFileID + } + + log.Debugf(" kstack[%d] = %v+%x (%v+%x)", i, string(mod), addr, symbol, offs) + + hostFileID := host.CalculateKernelFileID(fileID) + t.processManager.FileIDMapper.Set(hostFileID, fileID) + + trace.Frames[i] = host.Frame{ + File: hostFileID, + Lineno: libpf.AddressOrLineno(addr), + Type: libpf.KernelFrame, + } + + // Kernel frame PCs need to be adjusted by -1. This duplicates logic done in the trace + // converter. This should be fixed with PF-1042. + if foundSymbol && foundFileID { + t.reportFallbackKernelSymbol(fileID, symbol, trace.Frames[i].Lineno-1, + &kernelSymbolCacheHit, &kernelSymbolCacheMiss) + } + } + t.fallbackSymbolMiss.Add(kernelSymbolCacheMiss) + t.fallbackSymbolHit.Add(kernelSymbolCacheHit) + + return kstackLen, nil +} + +// reportFallbackKernelSymbol reports fallback symbols for kernel frames, after checking if the +// symbols were previously sent. +func (t *Tracer) reportFallbackKernelSymbol( + fileID libpf.FileID, symbolName libpf.SymbolName, frameAddress libpf.AddressOrLineno, + kernelSymbolCacheHit, kernelSymbolCacheMiss *uint64) { + frameID := libpf.NewFrameID(fileID, frameAddress) + + // Only report it if it's not in our LRU list of transmitted symbols. + if !t.transmittedFallbackSymbols.Contains(frameID) { + t.reporter.ReportFallbackSymbol(frameID, string(symbolName)) + + // There is no guarantee that the above report will be successfully delivered, but this + // should be sufficient for the time being. Other machines may succeed, and it's no big deal + // if we can't deliver 100% of symbols. + t.transmittedFallbackSymbols.Add(frameID, libpf.Void{}) + (*kernelSymbolCacheMiss)++ + return + } + (*kernelSymbolCacheHit)++ +} + +// enableEvent removes the entry of given eventType from the inhibitEvents map +// so that the eBPF code will send the event again. +func (t *Tracer) enableEvent(eventType int) { + inhibitEventsMap := t.ebpfMaps["inhibit_events"] + + // The map entry might not exist, so just ignore the potential error. + et := uint32(eventType) + _ = inhibitEventsMap.Delete(unsafe.Pointer(&et)) +} + +// monitorPIDEventsMap periodically iterates over the eBPF map pid_events, +// collects PIDs and writes them to the keys slice. +func (t *Tracer) monitorPIDEventsMap(keys *[]uint32) { + eventsMap := t.ebpfMaps["pid_events"] + var key, nextKey uint32 + var value bool + keyFound := true + deleteBatch := make(libpf.Set[uint32]) + + // Key 0 retrieves the very first element in the hash map as + // it is guaranteed not to exist in pid_events. + key = 0 + if err := eventsMap.NextKey(unsafe.Pointer(&key), unsafe.Pointer(&nextKey)); err != nil { + if errors.Is(err, cebpf.ErrKeyNotExist) { + log.Debugf("Empty pid_events map") + return + } + log.Fatalf("Failed to read from pid_events map: %v", err) + } + + for keyFound { + key = nextKey + + if err := eventsMap.Lookup(unsafe.Pointer(&key), unsafe.Pointer(&value)); err != nil { + log.Fatalf("Failed to lookup '%v' in pid_events: %v", key, err) + } + + // Lookup the next map entry before deleting the current one. + if err := eventsMap.NextKey(unsafe.Pointer(&key), unsafe.Pointer(&nextKey)); err != nil { + if !errors.Is(err, cebpf.ErrKeyNotExist) { + log.Fatalf("Failed to read from pid_events map: %v", err) + } + keyFound = false + } + + if !t.hasBatchOperations { + // Now that we have the next key, we can delete the current one. + if err := eventsMap.Delete(unsafe.Pointer(&key)); err != nil { + log.Fatalf("Failed to delete '%v' from pid_events: %v", key, err) + } + } else { + // Store to-be-deleted keys in a map so we can delete them all with a single + // bpf syscall. + deleteBatch[key] = libpf.Void{} + } + + // If we process keys inline with iteration (e.g. by sending them to t.pidEvents at this + // exact point), we may block sending to the channel, delay the iteration and may introduce + // race conditions (related to deletion). For that reason, keys are first collected and, + // after the iteration has finished, sent to the channel. + *keys = append(*keys, key) + } + + keysToDelete := len(deleteBatch) + if keysToDelete != 0 { + keys := libpf.MapKeysToSlice(deleteBatch) + if _, err := eventsMap.BatchDelete(keys, nil); err != nil { + log.Fatalf("Failed to batch delete %d entries from pid_events map: %v", + keysToDelete, err) + } + } +} + +// eBPFMetricsCollector retrieves the eBPF metrics, calculates their delta values, +// and translates eBPF IDs into Metric ID. +// Returns a slice of Metric ID/Value pairs. +func (t *Tracer) eBPFMetricsCollector( + translateIDs []metrics.MetricID, + previousMetricValue []metrics.MetricValue) []metrics.Metric { + metricsMap := t.ebpfMaps["metrics"] + metricsUpdates := make([]metrics.Metric, 0, len(translateIDs)) + + // Iterate over all known metric IDs + for ebpfID, metricID := range translateIDs { + var perCPUValues []uint64 + + // Checking for 'gaps' in the translation table. + // That allows non-contiguous metric IDs, e.g. after removal/deprecation of a metric ID. + if metricID == metrics.IDInvalid { + continue + } + + eID := uint32(ebpfID) + if err := metricsMap.Lookup(unsafe.Pointer(&eID), &perCPUValues); err != nil { + log.Errorf("Failed trying to lookup per CPU element: %v", err) + continue + } + value := metrics.MetricValue(0) + for _, val := range perCPUValues { + value += metrics.MetricValue(val) + } + + // The monitoring infrastructure expects instantaneous values (gauges). + // => for cumulative metrics (counters), send deltas of the observed values, so they + // can be interpreted as gauges. + if ebpfID < support.MetricIDBeginCumulative { + // We don't assume 64bit counters to overflow + deltaValue := value - previousMetricValue[ebpfID] + + // 0 deltas add no value when summed up for display purposes in the UI + if deltaValue == 0 { + continue + } + + previousMetricValue[ebpfID] = value + value = deltaValue + } + + // Collect the metrics for reporting + metricsUpdates = append(metricsUpdates, metrics.Metric{ + ID: metricID, + Value: value, + }) + } + + return metricsUpdates +} + +// loadBpfTrace parses a raw BPF trace into a `host.Trace` instance. +// +// If the raw trace contains a kernel stack ID, the kernel stack is also +// retrieved and inserted at the appropriate position. +func (t *Tracer) loadBpfTrace(raw []byte) *host.Trace { + frameListOffs := int(unsafe.Offsetof(C.Trace{}.frames)) + + if len(raw) < frameListOffs { + panic("trace record too small") + } + + frameSize := int(unsafe.Sizeof(C.Frame{})) + ptr := (*C.Trace)(unsafe.Pointer(unsafe.SliceData(raw))) + + // NOTE: can't do exact check here: kernel adds a few padding bytes to messages. + if len(raw) < frameListOffs+int(ptr.stack_len)*frameSize { + panic("unexpected record size") + } + + trace := &host.Trace{ + Comm: C.GoString((*C.char)(unsafe.Pointer(&ptr.comm))), + PID: libpf.PID(ptr.pid), + KTime: libpf.KTime(ptr.ktime), + } + + // Trace fields included in the hash: + // - PID, kernel stack ID, length & frame array. + // Intentionally excluded: + // - ktime, COMM + ptr.comm = [16]C.char{} + ptr.ktime = 0 + trace.Hash = host.TraceHash(xxh3.Hash128(raw).Lo) + + userFrameOffs := 0 + if ptr.kernel_stack_id >= 0 { + kstackLen, err := t.insertKernelFrames( + trace, uint32(ptr.stack_len), int32(ptr.kernel_stack_id)) + + if err != nil { + log.Errorf("Failed to get kernel stack frames for 0x%x: %v", trace.Hash, err) + } else { + userFrameOffs = int(kstackLen) + } + } + + // If there are no kernel frames, or reading them failed, we are responsible + // for allocating the columnar frame array. + if len(trace.Frames) == 0 { + trace.Frames = make([]host.Frame, ptr.stack_len) + } + + 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), + } + } + + return trace +} + +// StartMapMonitors starts goroutines for collecting metrics and monitoring eBPF +// maps for tracepoints, new traces, trace count updates and unknown PCs. +func (t *Tracer) StartMapMonitors(ctx context.Context, traceOutChan chan *host.Trace) error { + eventMetricCollector := t.startEventMonitor(ctx) + + startPollingPerfEventMonitor(ctx, t.ebpfMaps["trace_events"], t.intervals.TracePollInterval(), + int(config.SamplesPerSecond())*int(unsafe.Sizeof(C.Trace{})), func(rawTrace []byte) { + traceOutChan <- t.loadBpfTrace(rawTrace) + }) + + pidEvents := make([]uint32, 0) + periodiccaller.StartWithManualTrigger(ctx, t.intervals.MonitorInterval(), + t.triggerPIDProcessing, func(manualTrigger bool) { + t.enableEvent(support.EventTypeGenericPID) + t.monitorPIDEventsMap(&pidEvents) + + for _, ev := range pidEvents { + log.Debugf("=> PID: %v", ev) + t.pidEvents <- libpf.PID(ev) + } + + // Keep the underlying array alive to avoid GC pressure + pidEvents = pidEvents[:0] + }) + + // translateIDs is a translation table for eBPF IDs into Metric IDs. + // Index is the ebpfID, value is the corresponding metricID. + // nolint:lll + translateIDs := []metrics.MetricID{ + C.metricID_UnwindCallInterpreter: metrics.IDUnwindCallInterpreter, + C.metricID_UnwindErrZeroPC: metrics.IDUnwindErrZeroPC, + C.metricID_UnwindErrStackLengthExceeded: metrics.IDUnwindErrStackLengthExceeded, + C.metricID_UnwindErrBadTSDAddr: metrics.IDUnwindErrBadTLSAddr, + C.metricID_UnwindErrBadTPBaseAddr: metrics.IDUnwindErrBadTPBaseAddr, + C.metricID_UnwindNativeAttempts: metrics.IDUnwindNativeAttempts, + C.metricID_UnwindNativeFrames: metrics.IDUnwindNativeFrames, + C.metricID_UnwindNativeStackDeltaStop: metrics.IDUnwindNativeStackDeltaStop, + C.metricID_UnwindNativeErrLookupTextSection: metrics.IDUnwindNativeErrLookupTextSection, + C.metricID_UnwindNativeErrLookupIterations: metrics.IDUnwindNativeErrLookupIterations, + C.metricID_UnwindNativeErrLookupRange: metrics.IDUnwindNativeErrLookupRange, + C.metricID_UnwindNativeErrKernelAddress: metrics.IDUnwindNativeErrKernelAddress, + C.metricID_UnwindNativeErrWrongTextSection: metrics.IDUnwindNativeErrWrongTextSection, + C.metricID_UnwindNativeErrPCRead: metrics.IDUnwindNativeErrPCRead, + C.metricID_UnwindPythonAttempts: metrics.IDUnwindPythonAttempts, + C.metricID_UnwindPythonFrames: metrics.IDUnwindPythonFrames, + C.metricID_UnwindPythonErrBadPyThreadStateCurrentAddr: metrics.IDUnwindPythonErrBadPyThreadStateCurrentAddr, + C.metricID_UnwindPythonErrZeroThreadState: metrics.IDUnwindPythonErrZeroThreadState, + C.metricID_UnwindPythonErrBadThreadStateFrameAddr: metrics.IDUnwindPythonErrBadThreadStateFrameAddr, + C.metricID_UnwindPythonZeroFrameCodeObject: metrics.IDUnwindPythonZeroFrameCodeObject, + C.metricID_UnwindPythonErrBadCodeObjectArgCountAddr: metrics.IDUnwindPythonErrBadCodeObjectArgCountAddr, + C.metricID_UnwindNativeErrStackDeltaInvalid: metrics.IDUnwindNativeErrStackDeltaInvalid, + C.metricID_ErrEmptyStack: metrics.IDErrEmptyStack, + C.metricID_UnwindHotspotAttempts: metrics.IDUnwindHotspotAttempts, + C.metricID_UnwindHotspotFrames: metrics.IDUnwindHotspotFrames, + C.metricID_UnwindHotspotErrNoCodeblob: metrics.IDUnwindHotspotErrNoCodeblob, + C.metricID_UnwindHotspotErrInvalidCodeblob: metrics.IDUnwindHotspotErrInvalidCodeblob, + C.metricID_UnwindHotspotErrInterpreterFP: metrics.IDUnwindHotspotErrInterpreterFP, + C.metricID_UnwindHotspotErrLrUnwindingMidTrace: metrics.IDUnwindHotspotErrLrUnwindingMidTrace, + C.metricID_UnwindHotspotUnsupportedFrameSize: metrics.IDHotspotUnsupportedFrameSize, + C.metricID_UnwindNativeSmallPC: metrics.IDUnwindNativeSmallPC, + C.metricID_UnwindNativeErrLookupStackDeltaInnerMap: metrics.IDUnwindNativeErrLookupStackDeltaInnerMap, + C.metricID_UnwindNativeErrLookupStackDeltaOuterMap: metrics.IDUnwindNativeErrLookupStackDeltaOuterMap, + C.metricID_ErrBPFCurrentComm: metrics.IDErrBPFCurrentComm, + C.metricID_UnwindPHPAttempts: metrics.IDUnwindPHPAttempts, + C.metricID_UnwindPHPFrames: metrics.IDUnwindPHPFrames, + C.metricID_UnwindPHPErrBadCurrentExecuteData: metrics.IDUnwindPHPErrBadCurrentExecuteData, + C.metricID_UnwindPHPErrBadZendExecuteData: metrics.IDUnwindPHPErrBadZendExecuteData, + C.metricID_UnwindPHPErrBadZendFunction: metrics.IDUnwindPHPErrBadZendFunction, + C.metricID_UnwindPHPErrBadZendOpline: metrics.IDUnwindPHPErrBadZendOpline, + C.metricID_UnwindRubyAttempts: metrics.IDUnwindRubyAttempts, + C.metricID_UnwindRubyFrames: metrics.IDUnwindRubyFrames, + C.metricID_UnwindPerlAttempts: metrics.IDUnwindPerlAttempts, + C.metricID_UnwindPerlFrames: metrics.IDUnwindPerlFrames, + C.metricID_UnwindPerlTSD: metrics.IDUnwindPerlTLS, + C.metricID_UnwindPerlReadStackInfo: metrics.IDUnwindPerlReadStackInfo, + C.metricID_UnwindPerlReadContextStackEntry: metrics.IDUnwindPerlReadContextStackEntry, + C.metricID_UnwindPerlResolveEGV: metrics.IDUnwindPerlResolveEGV, + C.metricID_UnwindHotspotErrInvalidRA: metrics.IDUnwindHotspotErrInvalidRA, + C.metricID_UnwindV8Attempts: metrics.IDUnwindV8Attempts, + C.metricID_UnwindV8Frames: metrics.IDUnwindV8Frames, + C.metricID_UnwindV8ErrBadFP: metrics.IDUnwindV8ErrBadFP, + C.metricID_UnwindV8ErrBadJSFunc: metrics.IDUnwindV8ErrBadJSFunc, + C.metricID_UnwindV8ErrBadCode: metrics.IDUnwindV8ErrBadCode, + C.metricID_ReportedPIDsErr: metrics.IDReportedPIDsErr, + C.metricID_PIDEventsErr: metrics.IDPIDEventsErr, + C.metricID_UnwindNativeLr0: metrics.IDUnwindNativeLr0, + C.metricID_NumProcNew: metrics.IDNumProcNew, + C.metricID_NumProcExit: metrics.IDNumProcExit, + C.metricID_NumUnknownPC: metrics.IDNumUnknownPC, + C.metricID_NumGenericPID: metrics.IDNumGenericPID, + C.metricID_UnwindPythonErrBadCFrameFrameAddr: metrics.IDUnwindPythonErrBadCFrameFrameAddr, + C.metricID_MaxTailCalls: metrics.IDMaxTailCalls, + C.metricID_UnwindPythonErrNoProcInfo: metrics.IDUnwindPythonErrNoProcInfo, + C.metricID_UnwindPythonErrBadAutoTlsKeyAddr: metrics.IDUnwindPythonErrBadAutoTlsKeyAddr, + C.metricID_UnwindPythonErrReadThreadStateAddr: metrics.IDUnwindPythonErrReadThreadStateAddr, + C.metricID_UnwindPythonErrReadTsdBase: metrics.IDUnwindPythonErrReadTsdBase, + C.metricID_UnwindRubyErrNoProcInfo: metrics.IDUnwindRubyErrNoProcInfo, + C.metricID_UnwindRubyErrReadStackPtr: metrics.IDUnwindRubyErrReadStackPtr, + C.metricID_UnwindRubyErrReadStackSize: metrics.IDUnwindRubyErrReadStackSize, + C.metricID_UnwindRubyErrReadCfp: metrics.IDUnwindRubyErrReadCfp, + C.metricID_UnwindRubyErrReadEp: metrics.IDUnwindRubyErrReadEp, + C.metricID_UnwindRubyErrReadIseqBody: metrics.IDUnwindRubyErrReadIseqBody, + C.metricID_UnwindRubyErrReadIseqEncoded: metrics.IDUnwindRubyErrReadIseqEncoded, + C.metricID_UnwindRubyErrReadIseqSize: metrics.IDUnwindRubyErrReadIseqSize, + C.metricID_UnwindNativeErrLrUnwindingMidTrace: metrics.IDUnwindNativeErrLrUnwindingMidTrace, + C.metricID_UnwindNativeErrReadKernelModeRegs: metrics.IDUnwindNativeErrReadKernelModeRegs, + C.metricID_UnwindNativeErrChaseIrqStackLink: metrics.IDUnwindNativeErrChaseIrqStackLink, + C.metricID_UnwindV8ErrNoProcInfo: metrics.IDUnwindV8ErrNoProcInfo, + C.metricID_UnwindNativeErrBadUnwindInfoIndex: metrics.IDUnwindNativeErrBadUnwindInfoIndex, + } + + // previousMetricValue stores the previously retrieved metric values to + // calculate and store delta values. + previousMetricValue := make([]metrics.MetricValue, len(translateIDs)) + + periodiccaller.Start(ctx, t.intervals.MonitorInterval(), func() { + metrics.AddSlice(eventMetricCollector()) + metrics.AddSlice(t.eBPFMetricsCollector(translateIDs, previousMetricValue)) + + metrics.AddSlice([]metrics.Metric{ + { + ID: metrics.IDKernelFallbackSymbolLRUHit, + Value: metrics.MetricValue(t.fallbackSymbolHit.Swap(0)), + }, + { + ID: metrics.IDKernelFallbackSymbolLRUMiss, + Value: metrics.MetricValue(t.fallbackSymbolMiss.Swap(0)), + }, + }) + }) + + return nil +} + +// AttachTracer attaches the main tracer entry point to the perf interrupt events. The tracer +// entry point is always the native tracer. The native tracer will determine when to invoke the +// interpreter tracers based on address range information. +func (t *Tracer) AttachTracer(sampleFreq int) error { + tracerProg, ok := t.ebpfProgs["native_tracer_entry"] + if !ok { + return fmt.Errorf("entry program is not available") + } + + perfAttribute := new(perf.Attr) + perfAttribute.SetSampleFreq(uint64(sampleFreq)) + if err := perf.CPUClock.Configure(perfAttribute); err != nil { + return fmt.Errorf("failed to configure software perf event: %v", err) + } + + onlineCPUIDs, err := hostcpu.ParseCPUCoreIDs(hostcpu.CPUOnlinePath) + if err != nil { + return fmt.Errorf("failed to get online CPUs: %v", err) + } + + events := t.perfEntrypoints.WLock() + defer t.perfEntrypoints.WUnlock(&events) + for _, id := range onlineCPUIDs { + perfEvent, err := perf.Open(perfAttribute, perf.AllThreads, id, nil) + if err != nil { + return fmt.Errorf("failed to attach to perf event on CPU %d: %v", id, err) + } + if err := perfEvent.SetBPF(uint32(tracerProg.FD())); err != nil { + return fmt.Errorf("failed to attach eBPF program to perf event: %v", err) + } + *events = append(*events, perfEvent) + } + return nil +} + +// EnableProfiling enables the perf interrupt events with the attached eBPF programs. +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") + } + for id, event := range *events { + if err := event.Enable(); err != nil { + return fmt.Errorf("failed to enable perf event on CPU %d: %v", id, err) + } + } + return nil +} + +// probabilisticProfile performs a single iteration of probabilistic profiling. It will generate +// a random number between 0 and ProbabilisticThresholdMax-1 every interval. If the random +// number is smaller than threshold it will enable the frequency based sampling for this +// time interval. Otherwise the frequency based sampling events are disabled. +func (t *Tracer) probabilisticProfile(interval time.Duration, threshold uint) { + enableSampling := false + var probProfilingStatus = probProfilingDisable + + if rand.Intn(ProbabilisticThresholdMax) < int(threshold) { + enableSampling = true + probProfilingStatus = probProfilingEnable + log.Debugf("Start sampling for next interval (%v)", interval) + } else { + log.Debugf("Stop sampling for next interval (%v)", interval) + } + + events := t.perfEntrypoints.WLock() + defer t.perfEntrypoints.WUnlock(&events) + var enableErr, disableErr metrics.MetricValue + for _, event := range *events { + if enableSampling { + if err := event.Enable(); err != nil { + enableErr++ + log.Errorf("Failed to enable frequency based sampling: %v", + err) + } + continue + } + if err := event.Disable(); err != nil { + disableErr++ + log.Errorf("Failed to disable frequency based sampling: %v", err) + } + } + if enableErr != 0 { + metrics.Add(metrics.IDPerfEventEnableErr, enableErr) + } + if disableErr != 0 { + metrics.Add(metrics.IDPerfEventDisableErr, disableErr) + } + metrics.Add(metrics.IDProbProfilingStatus, + metrics.MetricValue(probProfilingStatus)) +} + +// StartProbabilisticProfiling periodically runs probabilistic profiling. +func (t *Tracer) StartProbabilisticProfiling(ctx context.Context, + interval time.Duration, threshold uint) { + metrics.Add(metrics.IDProbProfilingInterval, + metrics.MetricValue(interval.Seconds())) + + // Run a single iteration of probabilistic profiling to avoid needing + // to wait for the first interval to pass with periodiccaller.Start() + // before getting called. + t.probabilisticProfile(interval, threshold) + + periodiccaller.Start(ctx, interval, func() { + t.probabilisticProfile(interval, threshold) + }) +} + +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) +} diff --git a/utils/coredump/.gitignore b/utils/coredump/.gitignore new file mode 100644 index 00000000..b93f2754 --- /dev/null +++ b/utils/coredump/.gitignore @@ -0,0 +1,6 @@ +/coredump +/coredump.test +/testsources/java/*.class +/modulecache +/gdb-sysroot + diff --git a/utils/coredump/README.md b/utils/coredump/README.md new file mode 100644 index 00000000..a2fce040 --- /dev/null +++ b/utils/coredump/README.md @@ -0,0 +1,291 @@ +coredump testing +================ + +A coredump is an ELF file of type `ET_CORE` that contains a full state of the +process including information about memory mappings, thread CPU states, etc. +Basically, it is a full snapshot of a process at a specific time. + +In coredump testing, we compile the whole BPF unwinder code into a user-mode +executable, then use the information from a coredump to simulate a realistic +environment to test the unwinder code in. The coredump testing essentially +implements all required BPF helper functions in user-space, reading memory +and thread contexts from the coredump. + +The primary intention here is to have solid regression test coverage of our +unwinding code, but another useful side effect is being able to single-step +through the unwinder code in `gdb`. + +## Running the tests + +The coredump test suite is run as part of the top level Makefile's "make tests" +or `go test ./...` from the repository's root. All coredump test cases are +automatically picked up, ran, and verified. + +To run just the coredump tests without the remaining test suite – in this +directory – run: + +```bash +go test -v +``` + +To run an individual test, you can refer to it by its name: + +```bash +go test -v -run TestCoreDumps/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json +``` + +## Adding test cases + +This section describes the steps and requirements to add coredump tests. Tests +can either be created directly using the `new` subcommand of the helper tool in +this directory or by manually creating a coredump and then importing it. + +### Option 1: use `coredump new` + +This is the most straight-forward way to create new test cases. It requires +that the `gcore` utility that usually ships with the `gdb` package is installed. +This approach automatically adjusts the coredump filter as required and ignores +the `ulimit`, so no further preparation is required. + +When the application that you wish to create a test case for is in the desired +state, simply run: + +```bash +./coredump new -pid $(pgrep my-app-name) -name my-test-case-name +``` + +Note that `coredump new` doesn't actually upload the coredump data to the remote +coredump storage -- please refer to the [dedicated section][upload] for more +information. + +[upload]: #uploading-test-case-data + +If you run into issues mentioning `permission denied` you're probably lacking +privileges to debug the target process. In that case, simply run the command +with prepended `sudo` and fix the owner of the files created by running +`chown -R $UID:$GID .` in this directory. + +### Option 2: import manually created coredump + +We can also import a coredump that was previously created using one of +the options detailed in [the dedicated section][manually]. + +[manually]: #manually-creating-coredumps + +```bash +./coredump new -core path/to/coredump -name my-test-case-name +``` + +**Important:** this will also import all ELF executables that were loaded when +the coredump was created by attempting to find them on disk at the path where +they were loaded at execution time. If this is incorrect, for example because +the coredump was created on a different system where you absolutely can't run +the `coredump` helper tool directly, you should pass `-no-module-bundling`. +This will make the coredump tests fall back to memory-dumping the required ELF +modules. It should generally be avoided because the environment presented to +the testee differs from what it will observe in the real world, but is still +preferable to bundling the wrong executables with the test case. + +## Uploading test case data + +To allow for local experiments without the need to upload a ton of data with +every attempt, `coredump new` does **not** automatically upload the data for the +test-case to S3. Once you are happy with your test case, you can push the data +associated with the test case by running: + +```bash +./coredump upload -all +``` + +You don't have to worry about this breaking anything on other branches: the +underlying storage solution ensures that your uploaded files will never clash +with existing test cases. + +## Manually creating coredumps + +### Option 1: make the kernel save a coredump + +In this variant we essentially make the kernel think that the target application +crashed, causing the kernel to save a coredump for us. + +#### Setting the coredump filter + +Coredumps normally contain only the anonymous and modified pages to save disk +space. For our test cases, we want a full process memory dump that also contains +the pages mapped into the process from the ELF files. + +To get a full process memory dump one has to set the [`coredump_filter`][filter] +in advance by running: + +[filter]: https://man7.org/linux/man-pages/man5/core.5.html + +```bash +echo 0x3f > /proc/$PID/coredump_filter +``` + +**Note regarding PHP JIT:** if you want to add a PHP8+ coredump test you may +need to set the filter to `0xff` instead. The reason for this is that PHP8+ +uses shared pages for its JIT regions, and on some platforms like ARM64 the +memory dump may not be able to capture this information. + +#### Signals + +The kernel will generate a coredump when a process is killed with a signal that +defaults to dumping core, and the system configuration allows coredump +generation. From the list of [suitable signals][signals] +`SIGILL` or `SIGSYS` are typically a good choice. Some VMs like Java's HotSpot +hook other signals such as `SIGBUS`, `SIGSEGV`, `SIGABRT` and handle them +internally. If a specific signal doesn't yield the expected result, simply +try a different one. + +[signals]: https://man7.org/linux/man-pages/man7/signal.7.html + +#### Determine how coredumps are saved + +The coredump filename and location can be configured with the sysctl knob +[`kernel.core_pattern`][pattern]. Often the core is generated in the current +working directory, or in `/tmp` with the name `core`, potentially suffixed with +the PID and/or process name. On some distributions coredumps are managed by +systemd and must be extracted from an opaque storage via the +[`coredumpctl`][coredumpctl] helper. + +[pattern]: https://man7.org/linux/man-pages/man5/core.5.html +[coredumpctl]: https://www.freedesktop.org/software/systemd/man/coredumpctl.html + +To determine how coredumps are saved, you can run: + +```bash +sudo sysctl kernel.core_pattern +``` + +#### Adjusting the `ulimit` + +Normally the coredump generation is disabled via `ulimit`, and needs to be +adjusted first. To do so, in the same terminal that you'll later run the +application that you want to create a test case for, run: + +```bash +ulimit -c unlimited +``` + +#### Creating the coredump + +Via the executable name: + +```bash +pkill -ILL +``` + +Via the PID: + +```bash +kill -ILL +``` + +After running one the above commands, if everything went well, you should see +a line containing `(core dumped)` in the stdout of the target application. + +### Option 2: via GDB + +This variant is particularly interesting because it allows you to single-step +to a very particular state that you want test coverage for and then create a +coredump. To do so, simply use gdb as usual and then type `gcore` once the +application is in the desired state. The path of the created coredump will be +printed on stdout. + +The `gcore` command is also available as a standalone binary that can be +invoked directly from a shell (outside GDB) by typing: + +```bash +gcore $PID +``` + +### Option 3: from within BPF + +In some cases it's hard to use GDB to catch the application in a particular +state because it occurs very rarely. If the condition that you want to test +can be detected by a particular condition being true in the unwinder code, +you can use the [`DEBUG_CAPTURE_COREDUMP()` macro][macro] to kill and coredump +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 + +## Extracting coredumps or modules + +The actual coredumps are stored in an opaque storage solution and identified +within the test cases JSON file by their unique ID. The ID is stored in the +`coredump-ref` field for the coredump file itself and in the `ref` field for +the modules bundled with the test case (`modules` array). + +In order to retrieve a coredump or a module, simply find the associated ID in +the JSON file, then run: + +```bash +./coredump export-module -id -out path/to/write/file/to +``` + +## Debugging the BPF code + +To debug a failing test case it is advisable to build the tests as follows: + +```bash +CGO_CFLAGS='-O0 -g' go test -c -gcflags="all=-N -l" +``` + +This will build the tests as a standalone binary and disable all optimizations +which allows for a smooth single-stepping experience in both `gdb` and `dlv`. + +You can now debug the BPF C code by running a specific test case in GDB: + +```bash +gdb --args ./coredump.test -test.v -test.run \ + TestCoreDumps/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json +``` + +A breakpoint on `native_tracer_entry` tends to be a good entry-point for +single-stepping. + +## Cleaning up the coredump storage + +The `coredump` helper provides a subcommand for cleaning both the local and +the remote storage: + +```bash +./coredump clean +``` + +This will remove any data that is not referenced by any test case. The +subcommand defaults to only cleaning the local storage. To also clean the +remote data, pass the `-remote` argument: + +```bash +./coredump clean -remote +``` + +The remote deletion defaults to only deleting data that has been uploaded more +than 6 months ago. This ensures that you don't accidentally delete data for new +tests that have been proposed on a different branch that your current branch +isn't aware of, yet. + +To see what will be deleted before actually committing to it, you can pass the +`-dry-run` argument: + +```bash +./coredump clean -remote -dry-run +``` + +## Updating all test cases + +If a change in the unwinding causes many tests to produce different output, +you can use the `./coredump rebase` command to re-generate the thread array +for each test case based on current unwinding. + +## Updating the tests to support new BPF maps + +Please note that if your new feature adds new BPF maps then you will need to +add references to this map manually to this package. This is because we do not +currently support adding maps in an automated fashion. The best way to do this +is to look through existing code in this package and to see where existing code +refers to particular BPF maps. diff --git a/utils/coredump/analyze.go b/utils/coredump/analyze.go new file mode 100644 index 00000000..6a0c9bb4 --- /dev/null +++ b/utils/coredump/analyze.go @@ -0,0 +1,127 @@ +/* + * 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" + "encoding/json" + "flag" + "fmt" + "os" + "strconv" + "strings" + + "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/process" + "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" +) + +type analyzeCmd struct { + store *modulestore.Store + + coredumpPath string + casePath string + lwpFilter string + debugEbpf bool + debugLog bool + pid int +} + +func newAnalyzeCmd(store *modulestore.Store) *ffcli.Command { + args := &analyzeCmd{store: store} + + set := flag.NewFlagSet("analyze", flag.ExitOnError) + set.StringVar(&args.coredumpPath, "core", "", "Path of the coredump to analyze") + set.StringVar(&args.casePath, "case", "", "Path of the test case to analyze") + set.StringVar(&args.lwpFilter, "lwp", "", "Only unwind certain threads (comma separated)") + set.BoolVar(&args.debugEbpf, "debug-ebpf", false, "Enable eBPF debug printing") + set.BoolVar(&args.debugLog, "debug-log", false, "Enable HA debug logging") + set.IntVar(&args.pid, "pid", 0, "PID to analyze") + + return &ffcli.Command{ + Name: "analyze", + Exec: args.exec, + ShortUsage: "analyze [flags]", + ShortHelp: "Analyze a coredump file", + FlagSet: set, + } +} + +func (cmd *analyzeCmd) exec(context.Context, []string) (err error) { + // Validate arguments. + sourceArgCount := 0 + if cmd.coredumpPath != "" { + sourceArgCount++ + } + if cmd.pid != 0 { + sourceArgCount++ + } + if cmd.casePath != "" { + sourceArgCount++ + } + if sourceArgCount != 1 { + return fmt.Errorf("please specify either `-core`, `-case` or `-pid`") + } + + lwpFilter := libpf.Set[libpf.PID]{} + if cmd.lwpFilter != "" { + for _, lwp := range strings.Split(cmd.lwpFilter, ",") { + var parsed int64 + parsed, err = strconv.ParseInt(lwp, 10, 32) + if err != nil { + return fmt.Errorf("failed to parse LWP: %v", err) + } + lwpFilter[libpf.PID(parsed)] = libpf.Void{} + } + } + + if cmd.debugLog { + log.SetLevel(log.DebugLevel) + } + + var proc process.Process + if cmd.pid != 0 { + proc, err = process.NewPtrace(libpf.PID(cmd.pid)) + if err != nil { + return fmt.Errorf("failed to open pid `%d`: %w", cmd.pid, err) + } + } else if cmd.casePath != "" { + var testCase *CoredumpTestCase + testCase, err = readTestCase(cmd.casePath) + if err != nil { + return fmt.Errorf("failed to read test case: %w", err) + } + + proc, err = OpenStoreCoredump(cmd.store, testCase.CoredumpRef, testCase.Modules) + if err != nil { + return fmt.Errorf("failed to open coredump: %w", err) + } + } else { + proc, err = process.OpenCoredump(cmd.coredumpPath) + if err != nil { + return fmt.Errorf("failed to open coredump `%s`: %w", cmd.coredumpPath, err) + } + } + defer proc.Close() + + threads, err := ExtractTraces(context.Background(), proc, cmd.debugEbpf, lwpFilter) + if err != nil { + return fmt.Errorf("failed to extract traces: %w", err) + } + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + enc.SetEscapeHTML(false) + if err := enc.Encode(threads); err != nil { + return fmt.Errorf("JSON Marshall failed: %w", err) + } + + return nil +} diff --git a/utils/coredump/clean.go b/utils/coredump/clean.go new file mode 100644 index 00000000..9226c1be --- /dev/null +++ b/utils/coredump/clean.go @@ -0,0 +1,151 @@ +/* + * 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" + "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" +) + +type cleanCmd struct { + store *modulestore.Store + + // User-specified command line arguments. + local, remote, temp, dry bool + minAge uint64 +} + +func newCleanCmd(store *modulestore.Store) *ffcli.Command { + cmd := cleanCmd{store: store} + set := flag.NewFlagSet("clean", flag.ExitOnError) + set.BoolVar(&cmd.temp, "temp", true, "Delete lingering temporary files in the local cache") + set.BoolVar(&cmd.local, "local", true, "Clean the local cache") + set.BoolVar(&cmd.remote, "remote", false, "Clean the remote storage") + set.BoolVar(&cmd.dry, "dry-run", false, "Perform a dry-run (don't actually delete)") + set.Uint64Var(&cmd.minAge, "min-age", 6*30, + "Minimum module age to remove from remote, in days (default: 6 months)") + return &ffcli.Command{ + Name: "clean", + ShortUsage: "clean [flags]", + ShortHelp: "Remove unreferenced files in the module store", + FlagSet: set, + Exec: cmd.exec, + } +} + +func (cmd *cleanCmd) exec(context.Context, []string) error { + referenced, err := collectReferencedIDs() + if err != nil { + return fmt.Errorf("failed to collect referenced IDs") + } + + for _, task := range []struct { + enabled bool + fn func(libpf.Set[modulestore.ID]) error + }{ + {cmd.temp, cmd.cleanTemp}, + {cmd.local, cmd.cleanLocal}, + {cmd.remote, cmd.cleanRemote}, + } { + if task.enabled { + if err := task.fn(referenced); err != nil { + return err + } + } + } + + return nil +} + +func (cmd *cleanCmd) cleanTemp(libpf.Set[modulestore.ID]) error { + if err := cmd.store.RemoveLocalTempFiles(); err != nil { + return fmt.Errorf("failed to delete temp files: %w", err) + } + return nil +} + +func (cmd *cleanCmd) cleanLocal(referenced libpf.Set[modulestore.ID]) error { + localModules, err := cmd.store.ListLocalModules() + if err != nil { + return fmt.Errorf("failed to read local cache contents: %w", err) + } + + for module := range localModules { + if _, exists := referenced[module]; exists { + continue + } + + log.Infof("Removing local module `%s`", module.String()) + if !cmd.dry { + if err := cmd.store.RemoveLocalModule(module); err != nil { + return fmt.Errorf("failed to delete module: %w", err) + } + } + } + + return nil +} + +func (cmd *cleanCmd) cleanRemote(referenced libpf.Set[modulestore.ID]) error { + remoteModules, err := cmd.store.ListRemoteModules() + if err != nil { + return fmt.Errorf("failed to receive remote module list: %w", err) + } + + for module, lastChanged := range remoteModules { + if _, exists := referenced[module]; exists { + continue + } + if time.Since(lastChanged) < time.Duration(cmd.minAge)*24*time.Hour { + // In order to prevent us from accidentally deleting modules uploaded for tests + // proposed on other branches (but not yet merged with the current branch), we check + // whether the module was recently uploaded before deleting it. + log.Infof("Module `%s` is unreferenced, but was uploaded recently (%s). Skipping.", + module.String(), lastChanged) + continue + } + + log.Infof("Deleting unreferenced module `%s` (uploaded: %s)", module.String(), lastChanged) + if !cmd.dry { + if err = cmd.store.RemoveRemoteModule(module); err != nil { + return fmt.Errorf("failed to delete remote module: %w", err) + } + } + } + + return nil +} + +// collectReferencedIDs gathers a set of all modules referenced from all testcases. +func collectReferencedIDs() (libpf.Set[modulestore.ID], error) { + cases, err := findTestCases(false) + if err != nil { + return nil, fmt.Errorf("failed to find test cases: %w", err) + } + + referenced := libpf.Set[modulestore.ID]{} + for _, path := range cases { + test, err := readTestCase(path) + if err != nil { + return nil, fmt.Errorf("failed to read test case: %w", err) + } + + referenced[test.CoredumpRef] = libpf.Void{} + for _, module := range test.Modules { + referenced[module.Ref] = libpf.Void{} + } + } + + return referenced, nil +} diff --git a/utils/coredump/coredump.go b/utils/coredump/coredump.go new file mode 100644 index 00000000..829d2ab8 --- /dev/null +++ b/utils/coredump/coredump.go @@ -0,0 +1,267 @@ +/* + * 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" + "debug/elf" + "encoding/json" + "fmt" + "os" + "runtime" + "time" + "unsafe" + + 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" + pm "github.com/elastic/otel-profiling-agent/processmanager" + "github.com/elastic/otel-profiling-agent/support" +) + +// #include +// #include "../../support/ebpf/types.h" +// int unwind_traces(u64 id, int debug, u64 tp_base, void *ctx); +import "C" + +// sliceBuffer creates a Go slice from C buffer +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 + functionOffset uint32 + functionName string + fileName string +} + +// symbolizationCache collects and caches the interpreter manager's symbolization +// callbacks to be used for trace stringification. +type symbolizationCache struct { + files map[libpf.FileID]string + symbols map[symbolKey]symbolData +} + +func newSymbolizationCache() *symbolizationCache { + return &symbolizationCache{ + files: make(map[libpf.FileID]string), + symbols: make(map[symbolKey]symbolData), + } +} + +func (c *symbolizationCache) ExecutableMetadata(_ context.Context, fileID libpf.FileID, + fileName, _ string) { + c.files[fileID] = fileName +} + +func (c *symbolizationCache) FrameMetadata(fileID libpf.FileID, + addressOrLine libpf.AddressOrLineno, lineNumber libpf.SourceLineno, + functionOffset uint32, functionName, filePath string) { + key := symbolKey{fileID, addressOrLine} + data := symbolData{lineNumber, + functionOffset, functionName, filePath} + c.symbols[key] = data +} + +func (c *symbolizationCache) ReportFallbackSymbol(libpf.FrameID, string) {} + +func generateErrorMap() (map[libpf.AddressOrLineno]string, error) { + file, err := os.Open("../errors-codegen/errors.json") + if err != nil { + return nil, fmt.Errorf("failed to open errors.json: %w", err) + } + + type JSONError struct { + ID uint64 `json:"id"` + Name string `json:"name"` + } + + var errors []JSONError + if err = json.NewDecoder(file).Decode(&errors); err != nil { + return nil, fmt.Errorf("failed to parse errors.json: %w", err) + } + + out := make(map[libpf.AddressOrLineno]string, len(errors)) + for _, item := range errors { + out[libpf.AddressOrLineno(item.ID)] = item.Name + } + + return out, nil +} + +var errorMap xsync.Once[map[libpf.AddressOrLineno]string] + +func (c *symbolizationCache) symbolize(ty libpf.FrameType, fileID libpf.FileID, + lineNumber libpf.AddressOrLineno) (string, error) { + if ty.IsError() { + errMap, err := errorMap.GetOrInit(generateErrorMap) + if err != nil { + return "", fmt.Errorf("unable to construct error map: %v", err) + } + errName, ok := (*errMap)[lineNumber] + if !ok { + return "", fmt.Errorf( + "got invalid error code %d. forgot to `make generate`", lineNumber) + } + if ty == libpf.AbortFrame { + return fmt.Sprintf("", errName), nil + } + return fmt.Sprintf("", errName), nil + } + + if data, ok := c.symbols[symbolKey{fileID, lineNumber}]; ok { + return fmt.Sprintf("%s+%d in %s:%d", + data.functionName, data.functionOffset, + data.fileName, data.lineNumber), nil + } + + sourceFile, ok := c.files[fileID] + if !ok { + sourceFile = fmt.Sprintf("%08x", fileID) + } + return fmt.Sprintf("%s+0x%x", sourceFile, lineNumber), nil +} + +func ExtractTraces(ctx context.Context, pr process.Process, debug bool, + lwpFilter libpf.Set[libpf.PID]) ([]ThreadInfo, error) { + todo, cancel := context.WithCancel(ctx) + defer cancel() + + debugFlag := C.int(0) + if debug { + debugFlag = 1 + } + + 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"} { + dummyMaps[mapName] = &cebpf.Map{} + } + for i := support.StackDeltaBucketSmallest; i <= support.StackDeltaBucketLargest; i++ { + dummyMaps[fmt.Sprintf("exe_id_to_%d_stack_deltas", i)] = &cebpf.Map{} + } + + // In host agent we have set the default value for monitorInterval to 5 seconds. But as coredump + // nor the tests that are calling ExtractTraces() are initializing the reporter package we want + // to set monitorInterval to a higher value. + // monitorInterval is used in process manager to collect metrics for every monitorInterval + // and call functions within the reporter package to report these metrics. + // So if these functions in the reporter package are called in an uninitialized state the code + // panics. To avoid these panics we set monitorInterval to a high value so these reporter + // function are never used. + monitorInterval := time.Hour * 24 + + // Check compatibility. + pid := pr.PID() + machineData := pr.GetMachineData() + goarch := "" + switch machineData.Machine { + case elf.EM_X86_64: + goarch = "amd64" + case elf.EM_AARCH64: + goarch = "arm64" + default: + return nil, fmt.Errorf("unsupported target %v", machineData.Machine) + } + if runtime.GOARCH != goarch { + return nil, fmt.Errorf("traces must be extracted with a build [%s] of the same "+ + "architecture as the coredump [%s]", runtime.GOARCH, goarch) + } + threadInfo, err := pr.GetThreads() + if err != nil { + return nil, fmt.Errorf("failed to get thread info for process %d: %v", pid, err) + } + + // Interfaces for the managers + 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 + } + + manager, err := pm.New(todo, includeTracers, monitorInterval, &coredumpEbpfMaps, + pm.NewMapFileIDMapper(), symCache, coredumpOpener, false) + if err != nil { + return nil, fmt.Errorf("failed to get Interpreter manager: %v", err) + } + + manager.SynchronizeProcess(pr) + + info := make([]ThreadInfo, 0, len(threadInfo)) + for _, thread := range threadInfo { + if len(lwpFilter) > 0 { + if _, exists := lwpFilter[libpf.PID(thread.LWP)]; !exists { + continue + } + } + + // Get traces by calling ebpf code via CGO + ebpfCtx.resetTrace() + if rc := C.unwind_traces(ebpfCtx.PIDandTGID, debugFlag, C.u64(thread.TPBase), + unsafe.Pointer(&thread.GPRegs[0])); rc != 0 { + return nil, fmt.Errorf("failed to unwind lwp %v: %v", thread.LWP, rc) + } + // Symbolize traces with interpreter manager + trace := manager.ConvertTrace(&ebpfCtx.trace) + tinfo := ThreadInfo{LWP: thread.LWP} + for i := range trace.FrameTypes { + frame, err := symCache.symbolize(trace.FrameTypes[i], trace.Files[i], trace.Linenos[i]) + if err != nil { + return nil, err + } + tinfo.Frames = append(tinfo.Frames, frame) + } + info = append(info, tinfo) + } + + return info, nil +} diff --git a/utils/coredump/coredump_test.go b/utils/coredump/coredump_test.go new file mode 100644 index 00000000..c3a478b7 --- /dev/null +++ b/utils/coredump/coredump_test.go @@ -0,0 +1,42 @@ +/* + * 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) + }) + } +} diff --git a/utils/coredump/ebpfcode.go b/utils/coredump/ebpfcode.go new file mode 100644 index 00000000..809c1a4e --- /dev/null +++ b/utils/coredump/ebpfcode.go @@ -0,0 +1,126 @@ +/* + * 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 + +// This file contains the magic to include and build the ebpf C-code via CGO. +// +// The approach is to have a TLS variable (struct cgo_ctx *) that describe +// state of the eBPF program. This files defines that, and #includes all the +// eBPF code to be built in this unit. Additionally the main entry point +// "unwind_traces" that setups the TLS, and starts the unwinding is defined here. +// Also the tail call helper "bpf_tail_call" is overridden here, as it works in +// tandem with the main entry point's setjmp. + +/* +#define TESTING +#define TESTING_COREDUMP +#include +#include +#include +#include "../../support/ebpf/types.h" + +struct cgo_ctx { + jmp_buf jmpbuf; + u64 id, tp_base; + int ret; + int debug; +}; + +__thread struct cgo_ctx *__cgo_ctx; + +int bpf_log(const char *fmt, ...) +{ + void __bpf_log(const char *, int); + if (__cgo_ctx->debug) { + char msg[1024]; + size_t sz; + va_list va; + + va_start(va, fmt); + sz = vsnprintf(msg, sizeof msg, fmt, va); + __bpf_log(msg, sz); + va_end(va); + } +} + +#include "../../support/ebpf/interpreter_dispatcher.ebpf.c" +#include "../../support/ebpf/native_stack_trace.ebpf.c" +#include "../../support/ebpf/perl_tracer.ebpf.c" +#include "../../support/ebpf/php_tracer.ebpf.c" +#include "../../support/ebpf/python_tracer.ebpf.c" +#include "../../support/ebpf/hotspot_tracer.ebpf.c" +#include "../../support/ebpf/ruby_tracer.ebpf.c" +#include "../../support/ebpf/v8_tracer.ebpf.c" +#include "../../support/ebpf/system_config.ebpf.c" + +int unwind_traces(u64 id, int debug, u64 tp_base, void *ctx) +{ + struct cgo_ctx cgoctx; + + cgoctx.id = id; + cgoctx.ret = 0; + cgoctx.debug = debug; + cgoctx.tp_base = tp_base; + __cgo_ctx = &cgoctx; + if (setjmp(cgoctx.jmpbuf) == 0) { + cgoctx.ret = native_tracer_entry(ctx); + } + __cgo_ctx = 0; + return cgoctx.ret; +} + +// We don't want to call the actual `unwind_stop` function because it'd +// require us to properly emulate all the maps required for sending frames +// to usermode. +int coredump_unwind_stop(struct bpf_perf_event_data* ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + if (record->state.unwind_error) { + push_error(&record->trace, record->state.unwind_error); + } + + return 0; +} + +int bpf_tail_call(void *ctx, bpf_map_def *map, int index) +{ + int rc = 0; + switch (index) { + case PROG_UNWIND_STOP: + rc = coredump_unwind_stop(ctx); + break; + case PROG_UNWIND_NATIVE: + rc = unwind_native(ctx); + break; + case PROG_UNWIND_PERL: + rc = unwind_perl(ctx); + break; + case PROG_UNWIND_PHP: + rc = unwind_php(ctx); + break; + case PROG_UNWIND_PYTHON: + rc = unwind_python(ctx); + break; + case PROG_UNWIND_HOTSPOT: + rc = unwind_hotspot(ctx); + break; + case PROG_UNWIND_RUBY: + rc = unwind_ruby(ctx); + break; + case PROG_UNWIND_V8: + rc = unwind_v8(ctx); + break; + default: + return -1; + } + __cgo_ctx->ret = rc; + longjmp(__cgo_ctx->jmpbuf, 1); +} +*/ +import "C" diff --git a/utils/coredump/ebpfcontext.go b/utils/coredump/ebpfcontext.go new file mode 100644 index 00000000..01f7cb5d --- /dev/null +++ b/utils/coredump/ebpfcontext.go @@ -0,0 +1,156 @@ +/* + * 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 ( + "unsafe" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/libpf/process" + "github.com/elastic/otel-profiling-agent/libpf/remotememory" +) + +/* +#define TESTING_COREDUMP +#include +#include "../../support/ebpf/types.h" +#include "../../support/ebpf/extmaps.h" +*/ +import "C" + +// ebpfContext is the context for EBPF code regarding the process it's unwinding. +type ebpfContext struct { + // trace will contain the trace from the CGO executed eBPF unwinding code + trace host.Trace + + // remotememory provides access to the target process memory space + remoteMemory remotememory.RemoteMemory + + // PIDandTGID is the value for bpf_get_current_pid_tgid(), and is also the + // unique context ID passed from eBPF code to the helper functions written + // in Go to find the matching ebpfContext struct + PIDandTGID C.u64 + + // perCPURecord is the ebpf code PerCPURecord + perCPURecord unsafe.Pointer + + // unwindInfoArray is the ebpf map unwind_info_array + unwindInfoArray unsafe.Pointer + + // pidToPageMapping is the equivalent ebpf map implemented in Go. Special + // support is needed to handle the prefix lookup. + pidToPageMapping map[C.PIDPage]unsafe.Pointer + + // pidToPageMapping is the equivalent ebpf map implemented in Go. Special + // support is needed to handle the key structure. + stackDeltaPageToInfo map[C.StackDeltaPageKey]unsafe.Pointer + + // exeIDToStackDeltaMaps is the equivalent ebpf map implemented in Go. + // Implemented separately to handle the nested map, and improve performance. + exeIDToStackDeltaMaps map[C.u64]unsafe.Pointer + + // maps is the generic ebpf map implementation and implements all the + // ebpf maps that do not need special handling (maps defined above) + maps map[*C.bpf_map_def]map[any]unsafe.Pointer + + // systemConfig holds an instance of `SystemConfig`, the common map + // for storing configuration that is populated by the host-agent. + systemConfig unsafe.Pointer + + // stackDeltaFileID is context variable for nested map lookups + stackDeltaFileID C.u64 +} + +// ebpfContextMap is global mapping of EBPFContext id (PIDandTGID) to the actual data. +// This is needed to have the ebpf helpers written in Go to get access to the EBPFContext +// via given numeric ID (as Go pointers referring to memory with Go pointers cannot be +// passed directly to the C code). +var ebpfContextMap = map[C.u64]*ebpfContext{} + +// newEBPFContext creates new EBPF Context from given core dump image +func newEBPFContext(pr process.Process) *ebpfContext { + pid := pr.PID() + unwindInfoArray := C.unwind_info_array + ctx := &ebpfContext{ + trace: host.Trace{PID: pid}, + remoteMemory: pr.GetRemoteMemory(), + PIDandTGID: C.u64(pid) << 32, + pidToPageMapping: make(map[C.PIDPage]unsafe.Pointer), + stackDeltaPageToInfo: make(map[C.StackDeltaPageKey]unsafe.Pointer), + exeIDToStackDeltaMaps: make(map[C.u64]unsafe.Pointer), + maps: make(map[*C.bpf_map_def]map[any]unsafe.Pointer), + systemConfig: initSystemConfig(pr.GetMachineData()), + perCPURecord: C.malloc(C.sizeof_PerCPURecord), + unwindInfoArray: C.malloc(C.sizeof_UnwindInfo * C.ulong(unwindInfoArray.max_entries)), + } + ebpfContextMap[ctx.PIDandTGID] = ctx + return ctx +} + +func initSystemConfig(md process.MachineData) unsafe.Pointer { + rawPtr := C.malloc(C.sizeof_SystemConfig) + sv := (*C.SystemConfig)(rawPtr) + + sv.inverse_pac_mask = ^C.u64(md.CodePACMask) + // `tsd_get_base`, the function reading this field, is special-cased + // for coredump tests via `ifdefs`, so the value we set here doesn't matter. + sv.tpbase_offset = 0 + sv.drop_error_only_traces = C.bool(false) + + return rawPtr +} + +func (ec *ebpfContext) addMap(mapPtr *C.bpf_map_def, key any, value []byte) { + innerMap, ok := ec.maps[mapPtr] + if !ok { + innerMap = make(map[any]unsafe.Pointer) + ec.maps[mapPtr] = innerMap + } + innerMap[key] = C.CBytes(value) +} + +func (ec *ebpfContext) delMap(mapPtr *C.bpf_map_def, key any) { + if innerMap, ok := ec.maps[mapPtr]; ok { + if value, ok2 := innerMap[key]; ok2 { + C.free(value) + delete(innerMap, key) + } + } +} + +func (ec *ebpfContext) resetTrace() { + ec.trace.Frames = ec.trace.Frames[0:0] +} + +func (ec *ebpfContext) release() { + C.free(ec.perCPURecord) + C.free(ec.unwindInfoArray) + C.free(ec.systemConfig) + + for pidPage, pageInfo := range ec.pidToPageMapping { + C.free(pageInfo) + delete(ec.pidToPageMapping, pidPage) + } + for deltaKey, deltaInfo := range ec.stackDeltaPageToInfo { + C.free(deltaInfo) + delete(ec.stackDeltaPageToInfo, deltaKey) + } + + for fileID, stackDeltaMap := range ec.exeIDToStackDeltaMaps { + C.free(stackDeltaMap) + delete(ec.exeIDToStackDeltaMaps, fileID) + } + + for mapName, innerMap := range ec.maps { + for _, value := range innerMap { + C.free(value) + } + delete(ec.maps, mapName) + } + + delete(ebpfContextMap, ec.PIDandTGID) +} diff --git a/utils/coredump/ebpfhelpers.go b/utils/coredump/ebpfhelpers.go new file mode 100644 index 00000000..9e8c540e --- /dev/null +++ b/utils/coredump/ebpfhelpers.go @@ -0,0 +1,144 @@ +/* + * 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 + +// This file contains Go functions exported to the C ebpf code. This needs to be +// in separate file: +// Using //export in a file places a restriction on the preamble: since it is copied +// into two different C output files, it must not contain any definitions, only +// declarations. If a file contains both definitions and declarations, then the two +// output files will produce duplicate symbols and the linker will fail. To avoid +// this, definitions must be placed in preambles in other files, or in C source files. + +import ( + "encoding/hex" + "math/bits" + "unsafe" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/support" + + log "github.com/sirupsen/logrus" +) + +/* +#define TESTING_COREDUMP +#include "../../support/ebpf/types.h" +#include "../../support/ebpf/extmaps.h" +*/ +import "C" + +//nolint:gocritic +//export __bpf_log +func __bpf_log(buf unsafe.Pointer, sz C.int) { + log.Info(string(sliceBuffer(buf, sz))) +} + +//nolint:gocritic +//export __push_frame +func __push_frame(id, file, line C.u64, frameType 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), + }) + + return C.ERR_OK +} + +//nolint:gocritic +//export bpf_ktime_get_ns +func bpf_ktime_get_ns() C.ulonglong { + return C.ulonglong(libpf.GetKTime()) +} + +//nolint:gocritic +//export bpf_get_current_comm +func bpf_get_current_comm(buf unsafe.Pointer, sz C.int) C.int { + copy(sliceBuffer(buf, sz), "comm") + return 0 +} + +//nolint:gocritic +//export __bpf_probe_read +func __bpf_probe_read(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 { + return -1 + } + return 0 +} + +// stackDeltaInnerMap is a special map returned to C code to indicate that +// we are accessing one of nested maps in the exe_id_to_X_stack_deltas maps +var stackDeltaInnerMap = &C.bpf_map_def{ + key_size: 8, +} + +//nolint:gocritic +//export __bpf_map_lookup_elem +func __bpf_map_lookup_elem(id C.u64, mapdef *C.bpf_map_def, keyptr unsafe.Pointer) unsafe.Pointer { + ctx := ebpfContextMap[id] + switch mapdef { + case &C.pid_page_to_mapping_info: + key := (*C.PIDPage)(keyptr) + for key.prefixLen >= support.BitWidthPID { + if val, ok := ctx.pidToPageMapping[*key]; ok { + return val + } + key.prefixLen-- + shiftBits := support.BitWidthPID + support.BitWidthPage - key.prefixLen + mask := uint64(0xffffffffffffffff) << shiftBits + key.page &= C.ulonglong(bits.ReverseBytes64(mask)) + } + 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: + var key any + switch mapdef.key_size { + case 8: + key = *(*C.u64)(keyptr) + case 4: + key = *(*C.u32)(keyptr) + } + if innerMap, ok := ctx.maps[mapdef]; ok { + if val, ok := innerMap[key]; ok { + return val + } + } + case &C.stack_delta_page_to_info: + return ctx.stackDeltaPageToInfo[*(*C.StackDeltaPageKey)(keyptr)] + case &C.exe_id_to_8_stack_deltas, &C.exe_id_to_9_stack_deltas, &C.exe_id_to_10_stack_deltas, + &C.exe_id_to_11_stack_deltas, &C.exe_id_to_12_stack_deltas, &C.exe_id_to_13_stack_deltas, + &C.exe_id_to_14_stack_deltas, &C.exe_id_to_15_stack_deltas, &C.exe_id_to_16_stack_deltas, + &C.exe_id_to_17_stack_deltas, &C.exe_id_to_18_stack_deltas, &C.exe_id_to_19_stack_deltas, + &C.exe_id_to_20_stack_deltas, &C.exe_id_to_21_stack_deltas: + ctx.stackDeltaFileID = *(*C.u64)(keyptr) + return unsafe.Pointer(stackDeltaInnerMap) + case &C.unwind_info_array: + key := uintptr(*(*C.u32)(keyptr)) + return unsafe.Pointer(uintptr(ctx.unwindInfoArray) + key*C.sizeof_UnwindInfo) + case stackDeltaInnerMap: + key := uintptr(*(*C.u32)(keyptr)) + if deltas, ok := ctx.exeIDToStackDeltaMaps[ctx.stackDeltaFileID]; ok { + return unsafe.Pointer(uintptr(deltas) + key*C.sizeof_StackDelta) + } + case &C.metrics: + return unsafe.Pointer(uintptr(0)) + case &C.system_config: + return ctx.systemConfig + default: + log.Errorf("Map at 0x%x not found (looking up key '%v')", + mapdef, hex.Dump(sliceBuffer(keyptr, C.int(mapdef.key_size)))) + } + return unsafe.Pointer(uintptr(0)) +} diff --git a/utils/coredump/ebpfmaps.go b/utils/coredump/ebpfmaps.go new file mode 100644 index 00000000..56c279e4 --- /dev/null +++ b/utils/coredump/ebpfmaps.go @@ -0,0 +1,257 @@ +/* + * 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 ( + "fmt" + "math/bits" + "unsafe" + + "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" + pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" + "github.com/elastic/otel-profiling-agent/support" +) + +/* +#define TESTING_COREDUMP +#include +#include "../../support/ebpf/types.h" +#include "../../support/ebpf/extmaps.h" +*/ +import "C" + +// ebpfMapsCoredump implements the Stack Delta Manager and the Interpreter Manager +// required EbpfMaps interfaces to access the core dump. +type ebpfMapsCoredump struct { + ctx *ebpfContext +} + +var _ interpreter.EbpfHandler = &ebpfMapsCoredump{} + +func (emc *ebpfMapsCoredump) RemoveReportedPID(libpf.PID) { +} + +func (emc *ebpfMapsCoredump) CollectMetrics() []metrics.Metric { + return []metrics.Metric{} +} + +func (emc *ebpfMapsCoredump) UpdateInterpreterOffsets(ebpfProgIndex uint16, + fileID host.FileID, offsetRanges []libpf.Range) error { + offsetRange := offsetRanges[0] + value := C.OffsetRange{ + lower_offset: C.u64(offsetRange.Start), + upper_offset: C.u64(offsetRange.End), + program_index: C.u16(ebpfProgIndex), + } + emc.ctx.addMap(&C.interpreter_offsets, C.u64(fileID), libpf.SliceFrom(&value)) + return nil +} + +func (emc *ebpfMapsCoredump) UpdateProcData(t libpf.InterpType, pid libpf.PID, + ptr unsafe.Pointer) error { + switch t { + case libpf.Perl: + emc.ctx.addMap(&C.perl_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_PerlProcInfo)) + case libpf.PHP: + emc.ctx.addMap(&C.php_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_PHPProcInfo)) + case libpf.Python: + emc.ctx.addMap(&C.py_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_PyProcInfo)) + case libpf.HotSpot: + emc.ctx.addMap(&C.hotspot_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_HotspotProcInfo)) + case libpf.Ruby: + emc.ctx.addMap(&C.ruby_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_RubyProcInfo)) + case libpf.V8: + emc.ctx.addMap(&C.v8_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_V8ProcInfo)) + } + return nil +} + +func (emc *ebpfMapsCoredump) DeleteProcData(t libpf.InterpType, pid libpf.PID) error { + switch t { + case libpf.Perl: + emc.ctx.delMap(&C.perl_procs, C.u32(pid)) + case libpf.PHP: + emc.ctx.delMap(&C.php_procs, C.u32(pid)) + case libpf.Python: + emc.ctx.delMap(&C.py_procs, C.u32(pid)) + case libpf.HotSpot: + emc.ctx.delMap(&C.hotspot_procs, C.u32(pid)) + case libpf.Ruby: + emc.ctx.delMap(&C.ruby_procs, C.u32(pid)) + case libpf.V8: + emc.ctx.delMap(&C.v8_procs, C.u32(pid)) + } + return nil +} + +func (emc *ebpfMapsCoredump) UpdatePidInterpreterMapping(pid libpf.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 + // to be in big endian format. + bePid := bits.ReverseBytes32(uint32(pid)) + bePage := bits.ReverseBytes64(prefix.Key) + + biasAndUnwindProgram, err := support.EncodeBiasAndUnwindProgram(bias, interpreterProgram) + if err != nil { + return err + } + + cKey := C.PIDPage{ + prefixLen: C.u32(support.BitWidthPID + prefix.Length), + pid: C.u32(bePid), + page: C.u64(bePage), + } + + cValue := C.malloc(C.sizeof_PIDPageMappingInfo) + *(*C.PIDPageMappingInfo)(cValue) = C.PIDPageMappingInfo{ + file_id: C.u64(fileID), + bias_and_unwind_program: C.u64(biasAndUnwindProgram), + } + + ctx.pidToPageMapping[cKey] = cValue + return nil +} + +func (emc *ebpfMapsCoredump) DeletePidInterpreterMapping(pid libpf.PID, + prefix lpm.Prefix) error { + ctx := emc.ctx + // pid_page_to_mapping_info is a LPM trie and expects the pid and page + // to be in big endian format. + bePid := bits.ReverseBytes32(uint32(pid)) + bePage := bits.ReverseBytes64(prefix.Key) + + cKey := C.PIDPage{ + prefixLen: C.u32(support.BitWidthPID + prefix.Length), + pid: C.u32(bePid), + page: C.u64(bePage), + } + + if value, ok := ctx.pidToPageMapping[cKey]; ok { + C.free(value) + delete(ctx.pidToPageMapping, cKey) + } + + return nil +} + +// Stack delta management +func (emc *ebpfMapsCoredump) UpdateUnwindInfo(index uint16, info sdtypes.UnwindInfo) error { + unwindInfoArray := C.unwind_info_array + if C.uint(index) >= unwindInfoArray.max_entries { + return fmt.Errorf("unwind info array full (%d/%d items)", + index, unwindInfoArray.max_entries) + } + + cmd := (*C.UnwindInfo)(unsafe.Pointer(uintptr(emc.ctx.unwindInfoArray) + + uintptr(index)*C.sizeof_UnwindInfo)) + *cmd = C.UnwindInfo{ + opcode: C.u8(info.Opcode), + fpOpcode: C.u8(info.FPOpcode), + mergeOpcode: C.u8(info.MergeOpcode), + param: C.s32(info.Param), + fpParam: C.s32(info.FPParam), + } + return nil +} + +// Stack delta management +func (emc *ebpfMapsCoredump) UpdateExeIDToStackDeltas(fileID host.FileID, + deltaArrays []pmebpf.StackDeltaEBPF) (uint16, error) { + entSize := C.sizeof_StackDelta + deltas := C.malloc(C.size_t(len(deltaArrays) * entSize)) + for index, delta := range deltaArrays { + info := (*C.StackDelta)(unsafe.Pointer(uintptr(deltas) + uintptr(index*entSize))) + *info = C.StackDelta{ + addrLow: C.u16(delta.AddressLow), + unwindInfo: C.u16(delta.UnwindInfo), + } + } + ctx := emc.ctx + // The coredump framework has only one map because we don't have the kernel limitation + // of requiring fixed size inner maps. Return StackDeltaBucketSmallest as fixed mapID. + ctx.exeIDToStackDeltaMaps[C.u64(fileID)] = deltas + return support.StackDeltaBucketSmallest, nil +} + +func (emc *ebpfMapsCoredump) DeleteExeIDToStackDeltas(fileID host.FileID, + _ uint16) error { + ctx := emc.ctx + key := C.u64(fileID) + if value, ok := ctx.exeIDToStackDeltaMaps[key]; ok { + C.free(value) + delete(ctx.exeIDToStackDeltaMaps, key) + } + return nil +} + +func (emc *ebpfMapsCoredump) UpdateStackDeltaPages(fileID host.FileID, numDeltasPerPage []uint16, + mapID uint16, firstPageAddr uint64, +) error { + ctx := emc.ctx + firstDelta := uint32(0) + + for pageNumber, numDeltas := range numDeltasPerPage { + pageAddr := firstPageAddr + uint64(pageNumber)< sysroot fs path + for _, file := range files { + dest := path.Join(sysroot, strings.TrimPrefix(path.Clean(file.LocalPath), "/")) + + // Unpack file if not already done previously. + if _, err = os.Stat(dest); err != nil { + log.Infof("Unpacking %v", dest) + if err = os.MkdirAll(path.Dir(dest), 0o755); err != nil { + return fmt.Errorf("failed to create directory for %v: %v", dest, err) + } + + if err = cmd.store.UnpackModuleToPath(file.Ref, dest); err != nil { + return fmt.Errorf("failed to unpack module %v: %v", dest, err) + } + } + + // Read SONAME for symlink creation. + soName := readElfSoName(dest) + if soName != "" { + soNameMap[soName] = dest + } + } + + // Read main executable path from coredump. + cd, err := process.OpenCoredump(path.Join(sysroot, "core")) + if err != nil { + return fmt.Errorf("failed to inspect coredump: %v", err) + } + defer cd.Close() + executable := cd.MainExecutable() + if executable == "" { + return fmt.Errorf("failed to find main executable") + } + + // Unfortunately gdb doesn't use the mapping path and instead reads DSO + // names to load from ELF .dynamic section, then tries resolving things in + // a way similar to LD. DSOs are often behind multiple levels of symlinks, + // and we don't include those in our test cases. We thus have to recreate + // them according to what .dynamic section specifies to allow gdb to find + // everything that it needs. + symlinkRoot := path.Join(sysroot, "mapped") + if err = os.MkdirAll(symlinkRoot, 0o755); err != nil { + return fmt.Errorf("failed to create symlink dir: %v", err) + } + for soName, dsoPath := range soNameMap { + var dsoAbsPath string + dsoAbsPath, err = filepath.Abs(dsoPath) + if err != nil { + return fmt.Errorf("failed to get absolute path for DSO: %v", err) + } + + linkPath := path.Join(symlinkRoot, soName) + if _, err = os.Stat(linkPath); err == nil { + continue // assume exists + } + + log.Infof("Mapping DSO %v -> %v", soName, dsoPath) + if err = os.Symlink(dsoAbsPath, linkPath); err != nil { + return fmt.Errorf("failed to symlink: %v", err) + } + } + + if cmd.extractOnly { + return nil + } + + if len(test.Modules) == 0 { + log.Warn("Test-case doesn't bundle modules: gdb might not work well") + } + + gdbBin, err := exec.LookPath("gdb-multiarch") + if err != nil { + log.Warn("No gdb-multiarch installed. Falling back to regular gdb.") + gdbBin = "gdb" + } + + //nolint:gosec + gdb := exec.Command(gdbBin, + path.Join(sysroot, executable), + "-c", path.Join(sysroot, "core"), + "-iex", "set solib-search-path "+symlinkRoot, + "-iex", "set sysroot "+sysroot) + + gdb.Stdin = os.Stdin + gdb.Stdout = os.Stdout + gdb.Stderr = os.Stderr + + err = gdb.Run() + + if !cmd.keep { + if err2 := os.RemoveAll(sysroot); err2 != nil { + log.Errorf("Failed to remove sysroot: %v", err) + } + + // Only unlink the base directory if it's empty. os.Remove won't + // delete non-empty directories and error out in that case. + _ = os.Remove(sysrootBaseDir) + } + + return err +} + +// readElfSoName reads DT_SONAME from a given DSO on disk. +func readElfSoName(dsoPath string) string { + ef, err := pfelf.Open(dsoPath) + if err != nil { + log.Warnf("Failed to open ELF %v: %v", dsoPath, err) + return "" + } + defer ef.Close() + + if ef.Type != elf.ET_DYN { + return "" + } + + var soName []string + soName, err = ef.DynString(elf.DT_SONAME) + if err != nil { + log.Warnf("Failed to read DT_SONAME from %v: %v", dsoPath, err) + return "" + } + if len(soName) == 0 { + log.Warnf("DSO at %v doesn't specify an SONAME", dsoPath) + return "" + } + + return soName[0] +} diff --git a/utils/coredump/json.go b/utils/coredump/json.go new file mode 100644 index 00000000..e0c39b2f --- /dev/null +++ b/utils/coredump/json.go @@ -0,0 +1,108 @@ +/* + * 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. + */ + +// Defines the structures used in (de)serializing the coredump test cases. + +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" +) + +// CoredumpTestCase is the data structure generated from the core dump. +type CoredumpTestCase struct { + CoredumpRef modulestore.ID `json:"coredump-ref"` + Threads []ThreadInfo `json:"threads"` + Modules []ModuleInfo `json:"modules"` +} + +// ModuleInfo stores information about a module that was loaded when the coredump was created. +type ModuleInfo struct { + // Ref is a reference to the module's ELF binary in the module store. + Ref modulestore.ID `json:"ref"` + // LocalPath stores the path where the module was found when the coredump was created. + LocalPath string `json:"local-path"` +} + +// ThreadInfo describe stack state of one thread inside core dump. +type ThreadInfo struct { + LWP uint32 `json:"lwp"` + Frames []string `json:"frames"` +} + +// findTestCases returns a list of all test cases, optionally only for the current architecture. +func findTestCases(filterHostArch bool) ([]string, error) { + var arch string + if filterHostArch { + arch = runtime.GOARCH + } else { + arch = "*" + } + + pattern := fmt.Sprintf("./testdata/%s/*.json", arch) + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, fmt.Errorf("failed to locate test cases: %w", err) + } + + return matches, nil +} + +// makeTestCasePath creates the relative file path for a test case given a name. +func makeTestCasePath(name string) string { + return fmt.Sprintf("testdata/%s/%s.json", runtime.GOARCH, name) +} + +// writeTestCase writes a test case to disk. +func writeTestCase(path string, c *CoredumpTestCase, allowOverwrite bool) error { + flags := os.O_RDWR | os.O_CREATE + if allowOverwrite { + flags |= os.O_TRUNC + } else { + flags |= os.O_EXCL + } + + jsonFile, err := os.OpenFile(path, flags, 0o666) + if err != nil { + return fmt.Errorf("failed to create JSON file: %w", err) + } + + enc := json.NewEncoder(jsonFile) + enc.SetIndent("", " ") + enc.SetEscapeHTML(false) + if err := enc.Encode(c); err != nil { + return fmt.Errorf("JSON Marshall failed: %w", err) + } + + return nil +} + +// readTestCase reads a test case from the given path. +func readTestCase(path string) (*CoredumpTestCase, error) { + test := &CoredumpTestCase{} + if err := readJSON(path, test); err != nil { + return nil, err + } + return test, nil +} + +// readJSON reads a JSON file and unmarshalls it into the given object. +func readJSON(path string, to interface{}) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + dec := json.NewDecoder(f) + return dec.Decode(to) +} diff --git a/utils/coredump/main.go b/utils/coredump/main.go new file mode 100644 index 00000000..c3d2c577 --- /dev/null +++ b/utils/coredump/main.go @@ -0,0 +1,66 @@ +/* + * 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. + */ + +// coredump provides a tool for extracting stack traces from coredumps. +// It also includes a test suite to unit test pf-host-agent components against +// a set of coredumps to validate stack extraction code. + +package main + +import ( + "context" + "errors" + "flag" + "os" + + "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/peterbourgon/ff/v3/ffcli" + log "github.com/sirupsen/logrus" +) + +// moduleStoreS3Bucket defines the S3 bucket used for the module store. +const moduleStoreS3Bucket = "optimyze-proc-mem-testdata" + +func main() { + log.SetReportCaller(false) + log.SetFormatter(&log.TextFormatter{}) + + store := initModuleStore() + + root := ffcli.Command{ + Name: "coredump", + ShortUsage: "coredump [flags]", + ShortHelp: "Tool for creating and managing coredump test cases", + Subcommands: []*ffcli.Command{ + newAnalyzeCmd(store), + newCleanCmd(store), + newExportModuleCmd(store), + newNewCmd(store), + newRebaseCmd(store), + newUploadCmd(store), + newGdbCmd(store), + }, + Exec: func(context.Context, []string) error { + return flag.ErrHelp + }, + } + + if err := root.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + if !errors.Is(err, flag.ErrHelp) { + log.Fatalf("%v", err) + } + } +} + +func initModuleStore() *modulestore.Store { + cfg := aws.NewConfig().WithRegion("eu-central-1") + sess := session.Must(session.NewSession(cfg)) + s3Client := s3.New(sess) + return modulestore.New(s3Client, moduleStoreS3Bucket, "modulecache") +} diff --git a/utils/coredump/modulestore/id.go b/utils/coredump/modulestore/id.go new file mode 100644 index 00000000..6a519e9d --- /dev/null +++ b/utils/coredump/modulestore/id.go @@ -0,0 +1,91 @@ +/* + * 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 modulestore + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" +) + +// ID is used to uniquely identify a module in a `Store`. +type ID struct { + // hash stores the SHA256 sum of the module. It's distinct from the typical file ID we use + // everywhere else because the file ID is a partial hash, allowing for collisions. To also + // allow testing cases with colliding file IDs, the coredump tests use a more traditional + // checksum. + hash [32]byte +} + +// String implements the `fmt.Stringer` interface +func (id *ID) String() string { + return hex.EncodeToString(id.hash[:]) +} + +// IDFromString parses a string into an ID. +func IDFromString(s string) (ID, error) { + if len(s) != 64 { + return ID{}, fmt.Errorf("length %d doesn't match expected value (64)", len(s)) + } + + slice, err := hex.DecodeString(s) + if err != nil { + return ID{}, fmt.Errorf("failed to parse id: %w", err) + } + + var id ID + copy(id.hash[:], slice) + + return id, nil +} + +// MarshalJSON encodes the ID into JSON. +func (id *ID) MarshalJSON() ([]byte, error) { + return json.Marshal(id.String()) +} + +// UnmarshalJSON decodes JSON into an ID. +func (id *ID) UnmarshalJSON(b []byte) error { + var v string + if err := json.Unmarshal(b, &v); err != nil { + return err + } + parsed, err := IDFromString(v) + if err != nil { + return err + } + *id = parsed + return nil +} + +// calculateModuleID calculates the module ID for the given reader. +func calculateModuleID(reader io.Reader) (ID, error) { + buf := make([]byte, 16*1024) + hasher := sha256.New() + for { + n, err := reader.Read(buf) + if n == 0 { + break + } + + hasher.Write(buf[:n]) + + if err != nil { + if err == io.EOF { + break + } + return ID{}, fmt.Errorf("failed to read chunk: %w", err) + } + } + + var id ID + copy(id.hash[:], hasher.Sum(nil)) + + return id, nil +} diff --git a/utils/coredump/modulestore/reader.go b/utils/coredump/modulestore/reader.go new file mode 100644 index 00000000..3c495393 --- /dev/null +++ b/utils/coredump/modulestore/reader.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 modulestore + +import "io" + +// ModuleReader allows reading a module from the module store. +type ModuleReader struct { + io.ReaderAt + io.Closer + preferredReadSize uint + size uint +} + +// PreferredReadSize returns the preferred size and alignment of reads on this reader. +func (m *ModuleReader) PreferredReadSize() uint { + return m.preferredReadSize +} + +// Size returns the uncompressed size of the module. +func (m *ModuleReader) Size() uint { + return m.size +} diff --git a/utils/coredump/modulestore/store.go b/utils/coredump/modulestore/store.go new file mode 100644 index 00000000..d0df8f37 --- /dev/null +++ b/utils/coredump/modulestore/store.go @@ -0,0 +1,510 @@ +/* + * 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. + */ + +// The modulestore package implements `Store`, a storage for large binary files (modules). +// For more information, please refer to the documentation on the `Store` type. + +package modulestore + +import ( + "errors" + "fmt" + "io" + "os" + "path" + "strings" + "syscall" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + + "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" +) + +const ( + // localTempSuffix specifies the prefix appended to files in the local storage while they are + // still being written to. + localTempPrefix = "tmp." + // s3KeyPrefix defines the prefix prepended to all S3 keys. + s3KeyPrefix = "module-store/" + // s3ResultsPerPage defines how many results to request per page when listing objects. + s3ResultsPerPage = 1000 + // s3MaxPages defines the maximum number of pages to ever retrieve when listing objects. + s3MaxPages = 16 + // zstpakChunkSize determines the chunk size to use when compressing files. + zstpakChunkSize = 64 * 1024 +) + +// Store is a compressed storage for large binary files (modules). Upon inserting a new file, the +// caller receives a unique ID to identify the file by. This ID can then later be used to retrieve +// the module again. Files are transparently compressed upon insertion and lazily decompressed +// during reading. Modules can be pushed to a remote backing storage in the form of an S3 bucket. +// Modules present remotely but not locally are automatically downloaded when needed. +// +// It is safe to create multiple `Store` instances for the same local directory and remote bucket +// at the same time, also when created within multiple different applications. +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 +// full index of the modules in the remote S3 bucket is retrieved and cached as well. +func New(s3client *s3.S3, s3Bucket, localCachePath string) *Store { + return &Store{ + s3client: s3client, + bucket: s3Bucket, + localCachePath: localCachePath, + } +} + +// InsertModuleLocally places a file into the local cache, returning an ID to refer it by in the +// future. The file is **not** uploaded to the remote storage automatically. If the file was already +// previously present in the local store, the function returns the ID of the existing file. +func (store *Store) InsertModuleLocally(localPath string) (id ID, isNew bool, err error) { + var in *os.File + in, err = os.Open(localPath) + if err != nil { + return ID{}, false, fmt.Errorf("failed to open local file: %w", err) + } + + id, err = calculateModuleID(in) + if err != nil { + return ID{}, false, err + } + _, err = in.Seek(0, io.SeekStart) + if err != nil { + return ID{}, false, fmt.Errorf("failed to seek file back to start") + } + + present, err := store.IsPresentLocally(id) + if err != nil { + return ID{}, false, fmt.Errorf("failed to check whether the module exists locally: %w", err) + } + if present { + return id, false, nil + } + + // We first write the file with a suffix marking it as temporary, to prevent half-written + // files to persist in the local cache on crashes. + storePath := store.makeLocalPath(id) + out, err := os.CreateTemp(store.localCachePath, localTempPrefix) + if err != nil { + return ID{}, false, fmt.Errorf("failed to create file in local cache: %w", err) + } + defer out.Close() + + if err = zstpak.CompressInto(in, out, zstpakChunkSize); err != nil { + _ = os.Remove(storePath) + return ID{}, false, fmt.Errorf("failed to compress file: %w", err) + } + + if err = commitTempFile(out, storePath); err != nil { + return ID{}, false, err + } + + return id, true, nil +} + +// OpenReadAt opens a file in the store for random-access reading. +func (store *Store) OpenReadAt(id ID) (*ModuleReader, error) { + localPath, err := store.ensurePresentLocally(id) + if err != nil { + return nil, err + } + + file, err := zstpak.Open(localPath) + if err != nil { + return nil, fmt.Errorf("failed to open local file: %w", err) + } + + reader := &ModuleReader{ + ReaderAt: file, + Closer: file, + preferredReadSize: uint(file.ChunkSize()), + size: uint(file.UncompressedSize()), + } + + return reader, nil +} + +// OpenBufferedReadAt is a buffered version of `OpenReadAt`. +func (store *Store) OpenBufferedReadAt(id ID, cacheSizeBytes uint) ( + *ModuleReader, error) { + reader, err := store.OpenReadAt(id) + if err != nil { + return nil, err + } + + numChunks := cacheSizeBytes / reader.preferredReadSize + + if numChunks == 0 { + // Cache size too small for a full page: continue without buffering. + return reader, nil + } + + buffered, err := readatbuf.New(reader.ReaderAt, reader.preferredReadSize, numChunks) + if err != nil { + return nil, fmt.Errorf("failed to add buffering to the reader: %w", err) + } + + reader.ReaderAt = buffered + return reader, nil +} + +// UploadModule uploads a module from the local storage to the remote. If the module is already +// present, no operation is performed. +func (store *Store) UploadModule(id ID) error { + present, err := store.IsPresentRemotely(id) + if err != nil { + return fmt.Errorf("failed to check whether the module exists on remote: %w", err) + } + if present { + return nil + } + present, err = store.IsPresentLocally(id) + if err != nil { + return fmt.Errorf("failed to check whether the module exists locally: %w", err) + } + if !present { + return fmt.Errorf("the given module `%s` isn't present locally", id) + } + + localPath := store.makeLocalPath(id) + file, err := os.Open(localPath) + if err != nil { + return fmt.Errorf("failed to open local file: %w", err) + } + + _, err = store.s3client.PutObject(&s3.PutObjectInput{ + Bucket: aws.String(store.bucket), + Key: aws.String(makeS3Key(id)), + Body: file, + ContentType: aws.String("application/octet-stream"), + ContentDisposition: aws.String("attachment"), + }) + + if err != nil { + return fmt.Errorf("failed to upload file: %w", err) + } + + return nil +} + +// RemoveLocalModule removes a module from the local cache. No-op if not present. +func (store *Store) RemoveLocalModule(id ID) error { + present, err := store.IsPresentLocally(id) + if err != nil { + return fmt.Errorf("failed to check whether the module exists locally: %w", err) + } + if !present { + return nil + } + + if err := os.Remove(store.makeLocalPath(id)); err != nil { + return fmt.Errorf("failed to delete local file: %w", err) + } + + return nil +} + +// RemoveRemoteModule removes a module from the remote storage. No-op if not present. +func (store *Store) RemoveRemoteModule(id ID) error { + _, err := store.s3client.DeleteObject(&s3.DeleteObjectInput{ + Bucket: &store.bucket, + Key: aws.String(makeS3Key(id)), + }) + + if err != nil { + if isErrNoSuchKey(err) { + return nil + } + return fmt.Errorf("failed to delete file from remote: %w", err) + } + + return nil +} + +// UnpackModuleToPath extracts a module from the store to the given local path. +func (store *Store) UnpackModuleToPath(id ID, outPath string) error { + out, err := os.Create(outPath) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer out.Close() + + return store.UnpackModule(id, out) +} + +// UnpackModule extracts a module from the store, writing it to the given writer. +func (store *Store) UnpackModule(id ID, out io.Writer) error { + reader, err := store.OpenReadAt(id) + if err != nil { + return fmt.Errorf("failed to open module: %w", err) + } + defer reader.Close() + + // Create a sparse file if output is an empty file. + file, ok := out.(*os.File) + if ok { + pos, err := file.Seek(0, io.SeekCurrent) + if err == nil && pos == 0 { + err = unix.Ftruncate(int(file.Fd()), int64(reader.Size())) + if err != nil { + file = nil + } + } else { + file = nil + } + } + + chunk := make([]byte, reader.PreferredReadSize()) + offset := 0 + for { + n, err := reader.ReadAt(chunk, int64(offset)) + if err != nil { + if err == io.EOF { + chunk = chunk[:n] + } else { + return fmt.Errorf("failed to read module: %w", err) + } + } + if n == 0 { + break + } + + // Optimized sparse file path. + if file != nil && libpf.SliceAllEqual(chunk, 0) { + _, err = file.Seek(int64(len(chunk)), io.SeekCurrent) + if err != nil { + return fmt.Errorf("failed to seek: %v", err) + } + + offset += n + continue + } + + _, err = out.Write(chunk) + if err != nil { + return fmt.Errorf("failed to write to output file: %w", err) + } + + offset += n + } + + return nil +} + +// IsPresentRemotely checks whether a module is present in the remote data-store. +func (store *Store) IsPresentRemotely(id ID) (bool, error) { + _, err := store.s3client.HeadObject(&s3.HeadObjectInput{ + Bucket: &store.bucket, + Key: aws.String(makeS3Key(id)), + }) + + if err != nil { + if isErrNoSuchKey(err) { + return false, nil + } + return false, fmt.Errorf("failed to query module existence: %w", err) + } + + return true, nil +} + +// IsPresentLocally checks whether a module is present in the local cache. +func (store *Store) IsPresentLocally(id ID) (bool, error) { + _, err := os.Stat(store.makeLocalPath(id)) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("failed to stat local file: %w", err) + } + return true, nil +} + +// ListRemoteModules creates a map of all modules present in the remote storage and their date +// of last change. +func (store *Store) ListRemoteModules() (map[ID]time.Time, error) { + objectList, err := getS3ObjectList( + store.s3client, store.bucket, s3KeyPrefix, s3ResultsPerPage*s3MaxPages) + if err != nil { + return nil, fmt.Errorf("failed to retrieve object list: %w", err) + } + + 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") + } + + idText := strings.TrimPrefix(*object.Key, s3KeyPrefix) + id, err := IDFromString(idText) + if err != nil { + return nil, fmt.Errorf("failed to parse hash in S3 filename: %w", err) + } + modules[id] = *object.LastModified + } + + return modules, nil +} + +// ListLocalModules creates a set of all modules present in the local cache. +func (store *Store) ListLocalModules() (libpf.Set[ID], error) { + modules := libpf.Set[ID]{} + + moduleVisitor := func(id ID) error { + modules[id] = libpf.Void{} + return nil + } + unkVisitor := func(string) error { + return nil + } + if err := store.visitLocalModules(moduleVisitor, unkVisitor); err != nil { + return nil, err + } + + return modules, nil +} + +// RemoveLocalTempFiles removes all lingering temporary files that were never fully committed. +// +// If multiple instances of `Store` exist for the same cache directory, this may with uncommitted +// writes of the other instance. +func (store *Store) RemoveLocalTempFiles() error { + moduleVisitor := func(ID) error { + return nil + } + unkVisitor := func(unkPath string) error { + if !strings.HasPrefix(path.Base(unkPath), localTempPrefix) { + log.Warnf("`%s` file in local cache is neither a temp file nor a module", unkPath) + return nil + } + if err := os.Remove(unkPath); err != nil { + return fmt.Errorf("failed to remove file: %w", err) + } + return nil + } + return store.visitLocalModules(moduleVisitor, unkVisitor) +} + +// ensurePresentLocally makes sure a module is present locally, downloading it from the remote +// storage if required. On success, it returns the path to the compressed file in the local storage. +func (store *Store) ensurePresentLocally(id ID) (string, error) { + localPath := store.makeLocalPath(id) + present, err := store.IsPresentLocally(id) + 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 { + return "", fmt.Errorf("failed to create local file: %w", err) + } + defer file.Close() + + req := &s3.GetObjectInput{ + Bucket: aws.String(store.bucket), + Key: aws.String(makeS3Key(id)), + } + + downloader := s3manager.NewDownloaderWithClient(store.s3client) + _, err = downloader.Download(file, req) + if err != nil { + if isErrNoSuchKey(err) { + return "", errors.New("module doesn't exist in remote storage") + } + return "", fmt.Errorf("failed to download file from S3: %w", err) + } + + if err = commitTempFile(file, localPath); err != nil { + return "", err + } + + return localPath, nil +} + +// makeLocalPath creates the local cache path for the given ID. +func (store *Store) makeLocalPath(id ID) string { + return fmt.Sprintf("%s/%s", store.localCachePath, id.String()) +} + +// visitLocalModules visits all files in the local cache path. `moduleVisitor` is called for each +// file recognized as a valid module ID, `unkVisitor` is called with the full path of all other +// files in the path. +func (store *Store) visitLocalModules(moduleVisitor func(ID) error, + unkVisitor func(string) error) error { + files, err := os.ReadDir(store.localCachePath) + if err != nil { + return fmt.Errorf("failed to read files in local cache: %w", err) + } + + for _, file := range files { + id, err := IDFromString(file.Name()) + if err == nil { + err = moduleVisitor(id) + } else { + err = unkVisitor(path.Join(store.localCachePath, file.Name())) + } + if err != nil { + return nil + } + } + + return nil +} + +// makeS3Key creates the S3 key for the given module. +func makeS3Key(id ID) string { + return s3KeyPrefix + id.String() +} + +// commitTempFile makes sure that the given file is flushed to disk, then moves it to its final +// destination. +func commitTempFile(temp *os.File, finalPath string) error { + if err := syscall.Fsync(int(temp.Fd())); err != nil { + return fmt.Errorf("failed to flush file to disk: %w", err) + } + if err := os.Rename(temp.Name(), finalPath); err != nil { + return fmt.Errorf("failed to move file to final location: %w", err) + } + + return nil +} + +// isErrNoSuchKey checks whether the given AWS error indicates that the given key does not exist. +func isErrNoSuchKey(err error) bool { + // The documentation says that the API is supposed to return `NoSuchKey` if an object doesn't + // exist. However, in reality, the Go client instead simply inspects the HTTP status code and + // turns the 404 into "NotFound", without exposing the actual error code sent by the API. + // + // This unfortunately prevents us from telling a 404 from a non-existent key from a 404 caused + // by a non-existent bucket. We thus have to just assume it's `NoSuchKey`, since non-existent + // bucket should rarely happen in practice. + // + // For forward compatibility (in case this ever gets fixed), we also check for `NoSuchKey`. + var e awserr.Error + return errors.As(err, &e) && (e.Code() == "NoSuchKey" || e.Code() == "NotFound") +} diff --git a/utils/coredump/modulestore/util.go b/utils/coredump/modulestore/util.go new file mode 100644 index 00000000..974f95e0 --- /dev/null +++ b/utils/coredump/modulestore/util.go @@ -0,0 +1,46 @@ +/* + * 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 modulestore + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/service/s3" +) + +// getS3ObjectList gets all matching objects in an S3 bucket. +func getS3ObjectList(client *s3.S3, bucket, prefix string, itemLimit int) ([]*s3.Object, error) { + var objects []*s3.Object + var contToken *string + var batchSize int64 = s3ResultsPerPage + + for { + resp, err := client.ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: &bucket, + Prefix: &prefix, + MaxKeys: &batchSize, + ContinuationToken: contToken, + }) + + if err != nil { + return nil, fmt.Errorf("s3 request failed: %w", err) + } + + objects = append(objects, resp.Contents...) + + if int64(len(resp.Contents)) != batchSize { + break + } + if len(resp.Contents) > itemLimit { + return nil, fmt.Errorf("too many matching items in bucket") + } + + contToken = resp.ContinuationToken + } + + return objects, nil +} diff --git a/utils/coredump/new.go b/utils/coredump/new.go new file mode 100644 index 00000000..3bece5df --- /dev/null +++ b/utils/coredump/new.go @@ -0,0 +1,229 @@ +/* + * 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" + "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" +) + +// gcorePathPrefix specifies the path prefix we ask gcore to use when creating coredumps. +const gcorePathPrefix = "/tmp/coredump" + +type newCmd struct { + store *modulestore.Store + + // User-specified command line arguments. + coredumpPath string + pid uint64 + name string + importThreadInfo string + debugEbpf bool + noModuleBundling bool +} + +type trackedCoredump struct { + *process.CoredumpProcess + + prefix string + seen libpf.Set[string] +} + +func newTrackedCoredump(corePath, filePrefix string) (*trackedCoredump, error) { + core, err := process.OpenCoredump(corePath) + if err != nil { + return nil, err + } + + return &trackedCoredump{ + CoredumpProcess: core, + prefix: filePrefix, + seen: libpf.Set[string]{}, + }, nil +} + +func (tc *trackedCoredump) GetMappingFile(_ *process.Mapping) string { + return "" +} + +func (tc *trackedCoredump) CalculateMappingFileID(m *process.Mapping) (libpf.FileID, error) { + fid, err := pfelf.CalculateID(tc.prefix + m.Path) + if err == nil { + tc.seen[m.Path] = libpf.Void{} + } + return fid, err +} + +func (tc *trackedCoredump) OpenMappingFile(m *process.Mapping) (process.ReadAtCloser, error) { + rac, err := os.Open(tc.prefix + m.Path) + if err == nil { + tc.seen[m.Path] = libpf.Void{} + } + return rac, err +} + +func (tc *trackedCoredump) OpenELF(fileName string) (*pfelf.File, error) { + f, err := pfelf.Open(tc.prefix + fileName) + if err == nil { + tc.seen[fileName] = libpf.Void{} + } + return f, err +} + +func newNewCmd(store *modulestore.Store) *ffcli.Command { + args := &newCmd{store: store} + + set := flag.NewFlagSet("new", flag.ExitOnError) + set.StringVar(&args.coredumpPath, "core", "", "Path of the coredump to import") + set.Uint64Var(&args.pid, "pid", 0, "PID to create a fresh coredump for") + set.StringVar(&args.name, "name", "", "Name for the test case [required]") + set.StringVar(&args.importThreadInfo, "import-thread-info", "", "If this flag is specified, "+ + "the expected thread state is imported from another test case at the given path. If "+ + "omitted, the thread state is extracted by unwinding the coredump.") + set.BoolVar(&args.debugEbpf, "debug-ebpf", false, "Enable eBPF debug printing") + set.BoolVar(&args.noModuleBundling, "no-module-bundling", false, + "Don't bundle binaries from local disk with the testcase. Should be avoided in general, "+ + "but can be useful when importing coredumps from other systems.") + + return &ffcli.Command{ + Name: "new", + Exec: args.exec, + ShortUsage: "new [flags]", + ShortHelp: "Create or import a new test case", + FlagSet: set, + } +} + +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)") + } + if cmd.name == "" { + return fmt.Errorf("missing required argument `-name`") + } + + var corePath string + prefix := "" + if cmd.coredumpPath != "" { + corePath = cmd.coredumpPath + } else { + // No path provided: create a new dump. + corePath, err = dumpCore(cmd.pid) + if err != nil { + return fmt.Errorf("failed to create coredump: %w", err) + } + defer os.Remove(corePath) + prefix = fmt.Sprintf("/proc/%d/root/", cmd.pid) + } + + core, err := newTrackedCoredump(corePath, prefix) + if err != nil { + return fmt.Errorf("failed to open coredump: %w", err) + } + defer core.Close() + + testCase := &CoredumpTestCase{} + + testCase.Threads, err = ExtractTraces(context.Background(), core, cmd.debugEbpf, nil) + if err != nil { + return fmt.Errorf("failed to extract traces: %w", err) + } + + if cmd.importThreadInfo != "" { + var importTestCase *CoredumpTestCase + importTestCase, err = readTestCase(cmd.importThreadInfo) + if err != nil { + return fmt.Errorf("failed to read testcase to import thread info from: %w", err) + } + testCase.Threads = importTestCase.Threads + } + + testCase.CoredumpRef, _, err = cmd.store.InsertModuleLocally(corePath) + if err != nil { + return fmt.Errorf("failed to place coredump into local module storage: %w", err) + } + + if !cmd.noModuleBundling { + for fileName := range core.seen { + putModule(cmd.store, fileName, prefix, &testCase.Modules) + } + } + + path := makeTestCasePath(cmd.name) + if err = writeTestCase(path, testCase, false); err != nil { + return fmt.Errorf("failed to write test case: %w", err) + } + + log.Info("Test case successfully written!") + + return nil +} + +func dumpCore(pid uint64) (string, error) { + // Backup current coredump filter mask. + // https://man7.org/linux/man-pages/man5/core.5.html + coredumpFilterPath := fmt.Sprintf("/proc/%d/coredump_filter", pid) + prevMask, err := os.ReadFile(coredumpFilterPath) + if err != nil { + return "", fmt.Errorf("failed to read coredump filter: %w", err) + } + // Adjust coredump filter mask. + // nolint:gosec + err = os.WriteFile(coredumpFilterPath, []byte("0x3f"), 0o644) + if err != nil { + return "", fmt.Errorf("failed to write coredump filter: %w", err) + } + // Restore coredump filter mask upon leaving the function. + defer func() { + // nolint:gosec + err2 := os.WriteFile(coredumpFilterPath, prevMask, 0o644) + if err2 != nil { + log.Warnf("Failed to restore previous coredump filter: %v", err2) + } + }() + + // `gcore` only accepts a path-prefix, not an exact path. + // nolint:gosec + err = exec.Command("gcore", "-o", gcorePathPrefix, strconv.FormatUint(pid, 10)).Run() + if err != nil { + return "", fmt.Errorf("gcore failed: %w", err) + } + + return fmt.Sprintf("%s.%d", gcorePathPrefix, pid), nil +} + +func putModule(store *modulestore.Store, fileName, prefix string, modules *[]ModuleInfo) { + // Put the module into the module storage. + id, isNew, err := store.InsertModuleLocally(prefix + fileName) + if err != nil { + log.Errorf("Failed to place file into local module storage: %v", err) + return + } + + if isNew { + log.Infof("Module `%s` was newly added to local storage", fileName) + } else { + log.Infof("Module `%s` is already present in local storage", fileName) + } + + *modules = append(*modules, ModuleInfo{ + Ref: id, + LocalPath: fileName, + }) +} diff --git a/utils/coredump/rebase.go b/utils/coredump/rebase.go new file mode 100644 index 00000000..423dca2b --- /dev/null +++ b/utils/coredump/rebase.go @@ -0,0 +1,77 @@ +/* + * 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" + "flag" + "fmt" + "os/exec" + + "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" + "github.com/peterbourgon/ff/v3/ffcli" +) + +type rebaseCmd struct { + store *modulestore.Store + + allowDirty bool +} + +func newRebaseCmd(store *modulestore.Store) *ffcli.Command { + args := &rebaseCmd{store: store} + + set := flag.NewFlagSet("rebase", flag.ExitOnError) + set.BoolVar(&args.allowDirty, "allow-dirty", false, "Allow uncommitted changes in git") + + return &ffcli.Command{ + Name: "rebase", + Exec: args.exec, + ShortUsage: "rebase", + ShortHelp: "Update all test cases by running them and saving the current unwinding", + FlagSet: set, + } +} + +func (cmd *rebaseCmd) exec(context.Context, []string) (err error) { + cases, err := findTestCases(true) + if err != nil { + return fmt.Errorf("failed to find test cases: %w", err) + } + + if !cmd.allowDirty { + if err = exec.Command("git", "diff", "--quiet").Run(); err != nil { + return fmt.Errorf("refusing to work on a dirty source tree. " + + "please commit your changes first or pass `-allow-dirty` to ignore") + } + } + + for _, testCasePath := range cases { + var testCase *CoredumpTestCase + testCase, err = readTestCase(testCasePath) + if err != nil { + return fmt.Errorf("failed to read test case: %w", err) + } + + core, err := OpenStoreCoredump(cmd.store, testCase.CoredumpRef, testCase.Modules) + if err != nil { + return fmt.Errorf("failed to open coredump: %w", err) + } + + testCase.Threads, err = ExtractTraces(context.Background(), core, false, nil) + core.Close() + if err != nil { + return fmt.Errorf("failed to extract traces: %w", err) + } + + if err = writeTestCase(testCasePath, testCase, true); err != nil { + return fmt.Errorf("failed to write test case: %w", err) + } + } + + return nil +} diff --git a/utils/coredump/storecoredump.go b/utils/coredump/storecoredump.go new file mode 100644 index 00000000..0f160772 --- /dev/null +++ b/utils/coredump/storecoredump.go @@ -0,0 +1,93 @@ +/* + * 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 ( + "errors" + "fmt" + "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" + + log "github.com/sirupsen/logrus" +) + +type StoreCoredump struct { + *process.CoredumpProcess + + store *modulestore.Store + modules map[string]ModuleInfo +} + +var _ pfelf.ELFOpener = &StoreCoredump{} + +func (scd *StoreCoredump) openFile(path string) (process.ReadAtCloser, error) { + info, ok := scd.modules[path] + if !ok { + // The test case creator should have bundled everything. + // However, old test cases have no bundle at all, so give a warning + // only if some modules exists. + if len(scd.modules) != 0 { + log.Warnf("Store does not bundle %s", path) + } + return nil, fmt.Errorf("failed to open file `%s`: %w", path, os.ErrNotExist) + } + + // The module is available from store. + file, err := scd.store.OpenBufferedReadAt(info.Ref, 4*1024*1024) + if err != nil { + return nil, fmt.Errorf("failed to open file `%s`: %w", path, err) + } + return file, nil +} + +func (scd *StoreCoredump) OpenMappingFile(m *process.Mapping) (process.ReadAtCloser, error) { + return scd.openFile(m.Path) +} + +func (scd *StoreCoredump) OpenELF(path string) (*pfelf.File, error) { + file, err := scd.openFile(path) + if err == nil { + return pfelf.NewFile(file, 0, false) + } + if !errors.Is(err, os.ErrNotExist) { + return nil, err + } + // Fallback to the native CoredumpProcess + return scd.CoredumpProcess.OpenELF(path) +} + +func OpenStoreCoredump(store *modulestore.Store, coreFileRef modulestore.ID, modules []ModuleInfo) ( + process.Process, error) { + // Open the coredump from the module store. + reader, err := store.OpenBufferedReadAt(coreFileRef, 16*1024*1024) + if err != nil { + return nil, fmt.Errorf("failed to open coredump file reader: %w", err) + } + coreELF, err := pfelf.NewFile(reader, 0, false) + if err != nil { + return nil, fmt.Errorf("failed to open coredump ELF: %w", err) + } + core, err := process.OpenCoredumpFile(coreELF) + if err != nil { + return nil, fmt.Errorf("failed to open coredump: %w", err) + } + + moduleMap := map[string]ModuleInfo{} + for _, module := range modules { + moduleMap[module.LocalPath] = module + } + + return &StoreCoredump{ + CoredumpProcess: core, + + store: store, + modules: moduleMap, + }, nil +} diff --git a/utils/coredump/testdata/amd64/.gitkeep b/utils/coredump/testdata/amd64/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/utils/coredump/testdata/amd64/brokenstack.json b/utils/coredump/testdata/amd64/brokenstack.json new file mode 100644 index 00000000..12a36848 --- /dev/null +++ b/utils/coredump/testdata/amd64/brokenstack.json @@ -0,0 +1,28 @@ +{ + "coredump-ref": "20e6703ba5278290b6ec913da9605927ec95a4bde97c7ec22b30b9d49ffc4e9b", + "threads": [ + { + "lwp": 1458, + "frames": [ + "brokenstack+0x1148", + "brokenstack+0x1156", + "brokenstack+0x1176", + "" + ] + } + ], + "modules": [ + { + "ref": "46e6661eca1c28e6fd3ab6a586994d9d9eb2dd56a233ad1d1bf6b4ca68d91707", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "838ed8808d15fb09e04381d36a36eb45de3622d7d1d0bb1fac6722ccab2eb80e", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + }, + { + "ref": "e1972e610166390de54af6360388c72fbc2b2c6e51faaee9f07d3800f6b3cd67", + "local-path": "/home/admin/prodfiler/utils/coredump/testsources/c/brokenstack" + } + ] +} diff --git a/utils/coredump/testdata/amd64/glibc-signalframe.json b/utils/coredump/testdata/amd64/glibc-signalframe.json new file mode 100644 index 00000000..0952e2d0 --- /dev/null +++ b/utils/coredump/testdata/amd64/glibc-signalframe.json @@ -0,0 +1,36 @@ +{ + "coredump-ref": "1ac58fb10376e3f36837381be9ace59a92f8d1d57adcf3cadf43498dd3232a6d", + "threads": [ + { + "lwp": 7331, + "frames": [ + "libc.so.6+0xcf303", + "libc.so.6+0xd3c52", + "libc.so.6+0xd3b89", + "sig+0x1182", + "libc.so.6+0x3bf8f", + "libc.so.6+0xcf302", + "libc.so.6+0xd3c52", + "libc.so.6+0xd3b89", + "sig+0x11bc", + "libc.so.6+0x27189", + "libc.so.6+0x27244", + "sig+0x1090" + ] + } + ], + "modules": [ + { + "ref": "6cafb43b441840662cb52a035413f968c9cbb19c84725246e0d221186bdd4fa7", + "local-path": "/root/sig" + }, + { + "ref": "436bf9e45334ab7e44fa78ab06d2578af34eac8dece2b4cb373ba4be73dac86c", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "fa00e11432d68470e2b429605cff856659892611eec3a0908af7e077d2295c27", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + } + ] +} diff --git a/utils/coredump/testdata/amd64/graalvm-native.json b/utils/coredump/testdata/amd64/graalvm-native.json new file mode 100644 index 00000000..88231581 --- /dev/null +++ b/utils/coredump/testdata/amd64/graalvm-native.json @@ -0,0 +1,39 @@ +{ + "coredump-ref": "a9c4a72d50d0c9fa133b3d39756405ba431cfe329e39b3f9dd28ab9cbd256076", + "threads": [ + { + "lwp": 3327417, + "frames": [ + "hellograal+0x8a1202", + "" + ] + }, + { + "lwp": 3327418, + "frames": [ + "libc.so.6+0x899d9", + "libc.so.6+0x8c1cf", + "hellograal+0x4a8886", + "" + ] + } + ], + "modules": [ + { + "ref": "0190b9e6ab340b56865adbe256079ca78cb85c68a8ce7a02966d5c06b3d2bf19", + "local-path": "/usr/bin/hellograal" + }, + { + "ref": "a6b00848f58baa766d64522b36809d59da74d304bb97cf3222c2cd1275cefc19", + "local-path": "/usr/lib64/libc.so.6" + }, + { + "ref": "9330290f707b9f543f3e65e455ee1465a1825e71418b1e8756e01596e7a51d93", + "local-path": "/usr/lib64/libz.so.1.2.11" + }, + { + "ref": "9da4ee579134fd34f8b8ea536a02929d2711b387229b777cae63599b24c5229f", + "local-path": "/usr/lib64/ld-linux-x86-64.so.2" + } + ] +} diff --git a/utils/coredump/testdata/amd64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256msg2.json b/utils/coredump/testdata/amd64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256msg2.json new file mode 100644 index 00000000..a60cfbd3 --- /dev/null +++ b/utils/coredump/testdata/amd64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256msg2.json @@ -0,0 +1,371 @@ +{ + "coredump-ref": "09d5c8a1c65e62e61478355875ae0de820ad65744c1b046e7b9ebb10a25aa9d2", + "threads": [ + { + "lwp": 31777, + "frames": [ + "StubRoutines (2) [sha256_implCompressMB]+0 in :0", + "byte[] ShaShenanigans.hashRandomStuff()+4 in ShaShenanigans.java:30", + "void ShaShenanigans.shaShenanigans()+2 in ShaShenanigans.java:20", + "void ShaShenanigans.main(java.lang.String[])+0 in ShaShenanigans.java:13", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x81e991", + "libjvm.so+0x8b78f9", + "libjvm.so+0x8ba68f", + "libjli.so+0x4591", + "libjli.so+0x7ae8", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31776, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x8aac2", + "libjli.so+0x8619", + "libjli.so+0x586c", + "libjli.so+0x641e", + "java+0x1205", + "libc.so.6+0x27189", + "libc.so.6+0x27244", + "java+0x12a0" + ] + }, + { + "lwp": 31778, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x90b6f", + "libjvm.so+0xcc5151", + "libjvm.so+0xf662f6", + "libjvm.so+0xf6636d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31779, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x883f7", + "libjvm.so+0xc1d3a2", + "libjvm.so+0xbcc9b8", + "libjvm.so+0x6ff0d9", + "libjvm.so+0x5edffa", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31780, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x90b6f", + "libjvm.so+0xcc5151", + "libjvm.so+0xf662f6", + "libjvm.so+0xf6636d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31781, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x90b6f", + "libjvm.so+0xcc5151", + "libjvm.so+0x700fa3", + "libjvm.so+0x5edffa", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31782, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1d30f", + "libjvm.so+0xbcc9b8", + "libjvm.so+0x753724", + "libjvm.so+0x753927", + "libjvm.so+0x5edffa", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31783, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1d30f", + "libjvm.so+0xbcc9b8", + "libjvm.so+0xf390cc", + "libjvm.so+0xf39c4f", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31784, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x883f7", + "libjvm.so+0xc1d3a2", + "libjvm.so+0xbcca5e", + "libjvm.so+0x8efe59", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x81e991", + "libjvm.so+0x820011", + "libjvm.so+0x8e6482", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31785, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x883f7", + "libjvm.so+0xc1ca8a", + "libjvm.so+0xbf08f4", + "libjvm.so+0xe5e251", + "libjvm.so+0x8e7a2e", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x81e991", + "libjvm.so+0x820011", + "libjvm.so+0x8e6482", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31786, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x90b6f", + "libjvm.so+0xcc5151", + "libjvm.so+0xdd55d1", + "libjvm.so+0xc06864", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31787, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x883f7", + "libjvm.so+0xc1d3a2", + "libjvm.so+0xbcc9b8", + "libjvm.so+0xcc5b18", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31788, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1d30f", + "libjvm.so+0xbcc9b8", + "libjvm.so+0xbc0753", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31789, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1d30f", + "libjvm.so+0xbcca5e", + "libjvm.so+0x5d35c4", + "libjvm.so+0x5d643b", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31790, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1d30f", + "libjvm.so+0xbcca5e", + "libjvm.so+0x5d35c4", + "libjvm.so+0x5d643b", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31791, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1d30f", + "libjvm.so+0xbcc9b8", + "libjvm.so+0xe52f7e", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31792, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x883f7", + "libjvm.so+0xc1d3a2", + "libjvm.so+0xbcc9b8", + "libjvm.so+0xbe2a09", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31793, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1d30f", + "libjvm.so+0xbcc9b8", + "libjvm.so+0xbe24d3", + "libjvm.so+0xbe25bd", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + }, + { + "lwp": 31794, + "frames": [ + "libc.so.6+0x85d36", + "libc.so.6+0x886db", + "libjvm.so+0xc1cc97", + "libjvm.so+0xbf04c9", + "libjvm.so+0xe5e251", + "libjvm.so+0x8e7a2e", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x81e991", + "libjvm.so+0x820011", + "libjvm.so+0x8e6482", + "libjvm.so+0xeb238d", + "libjvm.so+0xeb5a4a", + "libjvm.so+0xc12c70", + "libc.so.6+0x88fd3", + "libc.so.6+0x1095bb" + ] + } + ], + "modules": [ + { + "ref": "b3598607138bb34e63b284d3eafb6f92978f824cc3653726d958a0ca29657d3e", + "local-path": "/usr/lib/jvm/java-17-openjdk-amd64/lib/libjimage.so" + }, + { + "ref": "fa00e11432d68470e2b429605cff856659892611eec3a0908af7e077d2295c27", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + }, + { + "ref": "4c66facd3d56ee90553297e81fa6837400c3fcfaf6e7f099e6904550c61fc58f", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "436bf9e45334ab7e44fa78ab06d2578af34eac8dece2b4cb373ba4be73dac86c", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "bbdc62edab66d3073dbb3614cbd1877ff99675c71448d1709de3c479ef713e15", + "local-path": "/usr/lib/jvm/java-17-openjdk-amd64/lib/libjli.so" + }, + { + "ref": "2bd2c307e135407cb14966043860c796ceae5c457e31472f3a917105bbe4050a", + "local-path": "/usr/lib/jvm/java-17-openjdk-amd64/lib/server/libjvm.so" + }, + { + "ref": "8b7e66a8f391da9240ea76f9a7863fc1beeca38eaf308ab509677ef19d3aaad0", + "local-path": "/usr/lib/x86_64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "7e2a72b4c4b38c61e6962de6e3f4a5e9ae692e732c68deead10a7ce2135a7f68", + "local-path": "/usr/lib/x86_64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "d41ecd03f0393631cf6cece80e7ea08d004090010128d618eb432c08dd72e164", + "local-path": "/usr/lib/jvm/java-17-openjdk-amd64/bin/java" + }, + { + "ref": "5df9c981aa49961ee777f40a8b21832baab03ebb293a87489ff1ff127ed789a1", + "local-path": "/usr/lib/jvm/java-17-openjdk-amd64/lib/libzip.so" + }, + { + "ref": "c9ab87b6ce811bc65fd3f0bef39f0bbb2472dbf2002968058ce8bca8d00a9900", + "local-path": "/usr/lib/jvm/java-17-openjdk-amd64/lib/libjava.so" + }, + { + "ref": "2b7d4c5c549898f3fff509105d7c5b283f0cfcaa83d38954dc8690bd5aba88ed", + "local-path": "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30" + }, + { + "ref": "d907ecd0cc0bb95a7fd93d250992e66edb63a8a5af44602b083f8d4398010237", + "local-path": "/usr/lib/jvm/java-17-openjdk-amd64/lib/libjsvml.so" + } + ] +} diff --git a/utils/coredump/testdata/amd64/java11.25812.json b/utils/coredump/testdata/amd64/java11.25812.json new file mode 100644 index 00000000..abb00b3a --- /dev/null +++ b/utils/coredump/testdata/amd64/java11.25812.json @@ -0,0 +1,340 @@ +{ + "coredump-ref": "077a380b756b6898860a302c0ccdd36f53621e265be5978f44da40f21c725ba1", + "threads": [ + { + "lwp": 25815, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xba16ba", + "libjvm.so+0xb51f17", + "libjvm.so+0xb52978", + "libjvm.so+0x72004b", + "libjvm.so+0x5ffba9", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25813, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb9feb6", + "libjvm.so+0x8d78e3", + "void java.lang.Thread.sleep(long)+0 in Thread.java:0", + "int Lambda1.lambda$comparator1$0(java.lang.Double, java.lang.Double)+0 in Lambda1.java:10", + "int Lambda1$$Lambda$1.compare(java.lang.Object, java.lang.Object)+0 in :0", + "int java.util.TimSort.countRunAndMakeAscending(java.lang.Object[], int, int, java.util.Comparator)+6 in TimSort.java:355", + "void java.util.TimSort.sort(java.lang.Object[], int, int, java.util.Comparator, java.lang.Object[], int, int)+8 in TimSort.java:220", + "void java.util.Arrays.sort(java.lang.Object[], int, int, java.util.Comparator)+7 in Arrays.java:1515", + "void java.util.ArrayList.sort(java.util.Comparator)+1 in ArrayList.java:1750", + "void java.util.Collections.sort(java.util.List, java.util.Comparator)+0 in Collections.java:179", + "void Lambda1.main(java.lang.String[])+3 in Lambda1.java:21", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x817f22", + "libjvm.so+0x898be3", + "libjvm.so+0x89b05c", + "libjli.so+0x4669", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25812, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x53882", + "libjli.so+0x8f8c", + "libjli.so+0x59e1", + "libjli.so+0x71c3", + "java+0x1212", + "ld-musl-x86_64.so.1+0x1ca02", + "java+0x129c" + ] + }, + { + "lwp": 25819, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb51fd8", + "libjvm.so+0xb52978", + "libjvm.so+0xda2242", + "libjvm.so+0xda299b", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25822, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc54781", + "libjvm.so+0xb928f9", + "libjvm.so+0xb85f84", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25817, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xba16ba", + "libjvm.so+0xb51f17", + "libjvm.so+0xb52978", + "libjvm.so+0x722c4b", + "libjvm.so+0x5ffba9", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25820, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xba16ba", + "libjvm.so+0xb51f17", + "libjvm.so+0xb529fd", + "libjvm.so+0x8cd15d", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:241", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:213", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x817f22", + "libjvm.so+0x81640c", + "libjvm.so+0x8c3ef9", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25824, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb51fd8", + "libjvm.so+0xb529fd", + "libjvm.so+0x5e99d0", + "libjvm.so+0x5ec794", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25827, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb51fd8", + "libjvm.so+0xb52978", + "libjvm.so+0xd27706", + "libjvm.so+0xd277ad", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25814, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc54781", + "libjvm.so+0xdc8a2c", + "libjvm.so+0xdc7ad8", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25825, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb51fd8", + "libjvm.so+0xb529fd", + "libjvm.so+0x5e99d0", + "libjvm.so+0x5ec794", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25821, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xba16ba", + "libjvm.so+0xb72674", + "libjvm.so+0xcec0b8", + "libjvm.so+0x8c56b5", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:170", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x817f22", + "libjvm.so+0x81640c", + "libjvm.so+0x8c3ef9", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25828, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb71f61", + "libjvm.so+0xcec0b8", + "libjvm.so+0x8c56b5", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+14 in CleanerImpl.java:148", + "void java.lang.Thread.run()+1 in Thread.java:829", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:134", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x817f22", + "libjvm.so+0x81640c", + "libjvm.so+0x8c3ef9", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25816, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc54781", + "libjvm.so+0xdc8a2c", + "libjvm.so+0xdc7ad8", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25826, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb51fd8", + "libjvm.so+0xb52978", + "libjvm.so+0xce4342", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25823, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xba16ba", + "libjvm.so+0xb51f17", + "libjvm.so+0xb52978", + "libjvm.so+0xc54f31", + "libjvm.so+0xd3012a", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 25818, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb9ef9d", + "libjvm.so+0xb51fd8", + "libjvm.so+0xb52978", + "libjvm.so+0x768368", + "libjvm.so+0x5ffba9", + "libjvm.so+0xd2d569", + "libjvm.so+0xb97c5d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/java12.20153.json b/utils/coredump/testdata/amd64/java12.20153.json new file mode 100644 index 00000000..e88ecac6 --- /dev/null +++ b/utils/coredump/testdata/amd64/java12.20153.json @@ -0,0 +1,364 @@ +{ + "coredump-ref": "052dbc5bcd198b22ea15bc9c44ff6dbbaedd37e1cb93da5a9ef388a551f12b87", + "threads": [ + { + "lwp": 20154, + "frames": [ + "StubRoutines (2) [arrayof_jbyte_disjoint_arraycopy]+0 in :0", + "void Prof1.main(java.lang.String[])+0 in Prof1.java:5", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x84e002", + "libjvm.so+0x8d01c4", + "libjvm.so+0x8d2ade", + "libjli.so+0x45d9", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20153, + "frames": [ + "ld-musl-x86_64.so.1+0x55352", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x53882", + "libjli.so+0x8f4c", + "libjli.so+0x5951", + "libjli.so+0x70eb", + "java+0x1212", + "ld-musl-x86_64.so.1+0x1ca02", + "java+0x129c" + ] + }, + { + "lwp": 20163, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc8cc81", + "libjvm.so+0xbc583c", + "libjvm.so+0xbba554", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20155, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc8cc81", + "libjvm.so+0xf1ad80", + "libjvm.so+0xf19ee8", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20169, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xba2cb2", + "libjvm.so+0xe2d328", + "libjvm.so+0x9028bf", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+14 in CleanerImpl.java:148", + "void java.lang.Thread.run()+1 in Thread.java:835", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:134", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x84e002", + "libjvm.so+0x84c53c", + "libjvm.so+0x90109b", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20164, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e23d", + "libjvm.so+0x61312f", + "libjvm.so+0x614e48", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20159, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e1b8", + "libjvm.so+0x7995b8", + "libjvm.so+0x627a09", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20156, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd3dca", + "libjvm.so+0xb7d897", + "libjvm.so+0xb7e1b8", + "libjvm.so+0x74d363", + "libjvm.so+0x627a09", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20158, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd3dca", + "libjvm.so+0xb7d897", + "libjvm.so+0xb7e1b8", + "libjvm.so+0x74feb1", + "libjvm.so+0x627a09", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20157, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc8cc81", + "libjvm.so+0xf1ad80", + "libjvm.so+0xf19ee8", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20168, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e1b8", + "libjvm.so+0xe7d196", + "libjvm.so+0xe7d23d", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20160, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e1b8", + "libjvm.so+0xef3313", + "libjvm.so+0xef39cb", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20167, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd3dca", + "libjvm.so+0xb7d897", + "libjvm.so+0xb7e1b8", + "libjvm.so+0xc8d3e8", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20162, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd3dca", + "libjvm.so+0xba2ee4", + "libjvm.so+0xe2d328", + "libjvm.so+0x9028bf", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:170", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x84e002", + "libjvm.so+0x84c53c", + "libjvm.so+0x90109b", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20171, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e23d", + "libjvm.so+0x61312f", + "libjvm.so+0x614e48", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20161, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd3dca", + "libjvm.so+0xb7d897", + "libjvm.so+0xb7e23d", + "libjvm.so+0x90c25d", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:241", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:213", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x84e002", + "libjvm.so+0x84c53c", + "libjvm.so+0x90109b", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20170, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e23d", + "libjvm.so+0x61312f", + "libjvm.so+0x614e48", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20166, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e1b8", + "libjvm.so+0xe22d7c", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 20165, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xbd16fd", + "libjvm.so+0xb7d948", + "libjvm.so+0xb7e23d", + "libjvm.so+0x61312f", + "libjvm.so+0x614e48", + "libjvm.so+0xe854fa", + "libjvm.so+0xe83089", + "libjvm.so+0xbcac6d", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/java13.11581.json b/utils/coredump/testdata/amd64/java13.11581.json new file mode 100644 index 00000000..39f6a2ca --- /dev/null +++ b/utils/coredump/testdata/amd64/java13.11581.json @@ -0,0 +1,417 @@ +{ + "coredump-ref": "c8e020f3f5a5ea26ffcab87d6d70f887b2c04e4f6fae7e2b011d1229599d40bb", + "threads": [ + { + "lwp": 11583, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xf8254c", + "libjvm.so+0xf815f8", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11586, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc342da", + "libjvm.so+0xbdf4bf", + "libjvm.so+0x75888e", + "libjvm.so+0x758a18", + "libjvm.so+0x626937", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11584, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc342da", + "libjvm.so+0xbdf4bf", + "libjvm.so+0x755d2d", + "libjvm.so+0x755e1f", + "libjvm.so+0x626937", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11592, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc342da", + "libjvm.so+0xbdf4bf", + "libjvm.so+0xcea7bb", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11581, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x53882", + "libjli.so+0x902a", + "libjli.so+0x59fc", + "libjli.so+0x72a6", + "java+0x1212", + "ld-musl-x86_64.so.1+0x1ca02", + "java+0x129c" + ] + }, + { + "lwp": 11590, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc33c8a", + "libjvm.so+0xc045f4", + "libjvm.so+0xe90148", + "libjvm.so+0x9187ad", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:170", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8663d2", + "libjvm.so+0x8649cc", + "libjvm.so+0x916d69", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11582, + "frames": [ + "vtable chunks+0 in :0", + "void Prof2.main(java.lang.String[])+8 in Prof2.java:16", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8663d2", + "libjvm.so+0x8e90a7", + "libjvm.so+0x8eb6fe", + "libjli.so+0x4669", + "libjli.so+0x8728", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11591, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xc251e5", + "libjvm.so+0xc18b44", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11587, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc3422f", + "libjvm.so+0xbdf4bf", + "libjvm.so+0x7b1251", + "libjvm.so+0x626937", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11589, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc342da", + "libjvm.so+0xbdf5e7", + "libjvm.so+0x921483", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:241", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:213", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8663d2", + "libjvm.so+0x8649cc", + "libjvm.so+0x916d69", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11594, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc3422f", + "libjvm.so+0xbdf5e7", + "libjvm.so+0x6108c4", + "libjvm.so+0x61330c", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11593, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc3422f", + "libjvm.so+0xbdf5e7", + "libjvm.so+0x6108c4", + "libjvm.so+0x61330c", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11588, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc3422f", + "libjvm.so+0xbdf4bf", + "libjvm.so+0xf58ac6", + "libjvm.so+0xf5921f", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11585, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xf8254c", + "libjvm.so+0xf815f8", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11602, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xf8254c", + "libjvm.so+0xf815f8", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11601, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xf8254c", + "libjvm.so+0xf815f8", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11603, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xf8254c", + "libjvm.so+0xf815f8", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11595, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc3422f", + "libjvm.so+0xbdf4bf", + "libjvm.so+0xe84951", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11600, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xf8254c", + "libjvm.so+0xf815f8", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11597, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc31575", + "libjvm.so+0xc043dd", + "libjvm.so+0xe90148", + "libjvm.so+0x9187ad", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+14 in CleanerImpl.java:148", + "void java.lang.Thread.run()+1 in Thread.java:830", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:134", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8663d2", + "libjvm.so+0x8649cc", + "libjvm.so+0x916d69", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11599, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xcea191", + "libjvm.so+0xf8254c", + "libjvm.so+0xf815f8", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11598, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc3422f", + "libjvm.so+0xbdf5e7", + "libjvm.so+0x6108c4", + "libjvm.so+0x61330c", + "libjvm.so+0xee3119", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 11596, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xc3422f", + "libjvm.so+0xbdf4bf", + "libjvm.so+0xee209c", + "libjvm.so+0xee2165", + "libjvm.so+0xee8304", + "libjvm.so+0xc2a3c6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/java14.3576.json b/utils/coredump/testdata/amd64/java14.3576.json new file mode 100644 index 00000000..eb174c97 --- /dev/null +++ b/utils/coredump/testdata/amd64/java14.3576.json @@ -0,0 +1,398 @@ +{ + "coredump-ref": "42083c77549bc7ea554d1288117a26fa152f6d99abb675ba7e356f36902c5db0", + "threads": [ + { + "lwp": 3577, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x58ac2", + "libjava.so+0x17807", + "libjava.so+0x172c3", + "libjava.so+0xf796", + "void java.io.FileOutputStream.writeBytes(byte[], int, int, boolean)+0 in FileOutputStream.java:0", + "void java.io.FileOutputStream.write(byte[], int, int)+0 in FileOutputStream.java:347", + "void java.io.BufferedOutputStream.flushBuffer()+1 in BufferedOutputStream.java:81", + "void java.io.BufferedOutputStream.flush()+0 in BufferedOutputStream.java:142", + "void java.io.PrintStream.write(byte[], int, int)+4 in PrintStream.java:570", + "void sun.nio.cs.StreamEncoder.writeBytes()+11 in StreamEncoder.java:242", + "void sun.nio.cs.StreamEncoder.implFlushBuffer()+1 in StreamEncoder.java:321", + "void sun.nio.cs.StreamEncoder.flushBuffer()+2 in StreamEncoder.java:110", + "void java.io.OutputStreamWriter.flushBuffer()+0 in OutputStreamWriter.java:181", + "void java.io.PrintStream.write(java.lang.String)+4 in PrintStream.java:699", + "void java.io.PrintStream.print(java.lang.String)+0 in PrintStream.java:863", + "void Deopt.Handle(int)+9 in Deopt.java:20", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.main(java.lang.String[])+3 in Deopt.java:31", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7a1cdd", + "libjvm.so+0x82e571", + "libjvm.so+0x830b8e", + "libjli.so+0x464c", + "libjli.so+0x8578", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3576, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x53882", + "libjli.so+0x8e4a", + "libjli.so+0x590c", + "libjli.so+0x7152", + "java+0x1212", + "ld-musl-x86_64.so.1+0x1ca02", + "java+0x129c" + ] + }, + { + "lwp": 3582, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e0af", + "libjvm.so+0x6ef7e1", + "libjvm.so+0x591777", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3578, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc27551", + "libjvm.so+0xe6ccac", + "libjvm.so+0xe6bdc8", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3579, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fb4a", + "libjvm.so+0xb2e0af", + "libjvm.so+0x68d38d", + "libjvm.so+0x68d47f", + "libjvm.so+0x591777", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3587, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc27551", + "libjvm.so+0xb70d08", + "libjvm.so+0xb652d4", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3581, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fb4a", + "libjvm.so+0xb2e0af", + "libjvm.so+0x68ff8e", + "libjvm.so+0x69011a", + "libjvm.so+0x591777", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3588, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fb4a", + "libjvm.so+0xb2e0af", + "libjvm.so+0xc27be0", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3592, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fb4a", + "libjvm.so+0xb2e0af", + "libjvm.so+0xb43d81", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3594, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7f565", + "libjvm.so+0xb50487", + "libjvm.so+0xd7b918", + "libjvm.so+0x85d81d", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+14 in CleanerImpl.java:148", + "void java.lang.Thread.run()+1 in Thread.java:832", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:134", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7a1cdd", + "libjvm.so+0x7a3498", + "libjvm.so+0x85bc79", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3586, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7f3ca", + "libjvm.so+0xb505a4", + "libjvm.so+0xd7b918", + "libjvm.so+0x85d81d", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:170", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7a1cdd", + "libjvm.so+0x7a3498", + "libjvm.so+0x85bc79", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3590, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e1d2", + "libjvm.so+0x57b6c4", + "libjvm.so+0x57dfac", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3589, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e1d2", + "libjvm.so+0x57b6c4", + "libjvm.so+0x57dfac", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3580, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0xc27551", + "libjvm.so+0xe6ccac", + "libjvm.so+0xe6bdc8", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3593, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e0af", + "libjvm.so+0xdcd6ac", + "libjvm.so+0xdcd775", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3601, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e1d2", + "libjvm.so+0x57b6c4", + "libjvm.so+0x57dfac", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3584, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fb4a", + "libjvm.so+0xb2e1d2", + "libjvm.so+0x865643", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:241", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:213", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7a1cdd", + "libjvm.so+0x7a3498", + "libjvm.so+0x85bc79", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3600, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e1d2", + "libjvm.so+0x57b6c4", + "libjvm.so+0x57dfac", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3583, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e0af", + "libjvm.so+0xe43a3a", + "libjvm.so+0xe440ef", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 3591, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0xb7fa9e", + "libjvm.so+0xb2e0af", + "libjvm.so+0xd700c3", + "libjvm.so+0xdce719", + "libjvm.so+0xdd3424", + "libjvm.so+0xb75cb6", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/java16.43723.json b/utils/coredump/testdata/amd64/java16.43723.json new file mode 100644 index 00000000..854dc02f --- /dev/null +++ b/utils/coredump/testdata/amd64/java16.43723.json @@ -0,0 +1,344 @@ +{ + "coredump-ref": "09d9e469f8c86f8b23bf8642a6d1749874a6f9c9bf139850c3dff44d0ae7581a", + "threads": [ + { + "lwp": 43723, + "frames": [ + "libc-2.31.so+0x4618b", + "libc-2.31.so+0x25858", + "libjvm.so+0x244002", + "libjvm.so+0xf2fa83", + "libjvm.so+0xf3036e", + "libjvm.so+0xf303a1", + "libjvm.so+0xdcd7fd", + "libc-2.31.so+0x4620f", + "libpthread-2.31.so+0xacd4", + "libjli.so+0x904e", + "libjli.so+0x5d80", + "libjli.so+0x76d4", + "java+0x12b2", + "libc-2.31.so+0x270b2", + "java+0x135d" + ] + }, + { + "lwp": 43726, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe072c", + "libjvm.so+0x712151", + "libjvm.so+0x7132c2", + "libjvm.so+0x601a4e", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43725, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xf5efdf", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43737, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe085a", + "libjvm.so+0x5e5565", + "libjvm.so+0x5e83b0", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43738, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0xe45ffa", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43724, + "frames": [ + "libc-2.31.so+0x11121f", + "libjava.so+0x16bb7", + "libjava.so+0x16610", + "libjava.so+0xeb1a", + "void java.io.FileOutputStream.writeBytes(byte[], int, int, boolean)+0 in FileOutputStream.java:0", + "void java.io.FileOutputStream.write(byte[], int, int)+0 in FileOutputStream.java:347", + "void java.io.BufferedOutputStream.flushBuffer()+1 in BufferedOutputStream.java:81", + "void java.io.BufferedOutputStream.flush()+0 in BufferedOutputStream.java:142", + "void java.io.PrintStream.write(byte[], int, int)+4 in PrintStream.java:570", + "void sun.nio.cs.StreamEncoder.writeBytes()+11 in StreamEncoder.java:242", + "void sun.nio.cs.StreamEncoder.implFlushBuffer()+1 in StreamEncoder.java:321", + "void sun.nio.cs.StreamEncoder.flushBuffer()+2 in StreamEncoder.java:110", + "void java.io.OutputStreamWriter.flushBuffer()+0 in OutputStreamWriter.java:178", + "void java.io.PrintStream.write(java.lang.String)+4 in PrintStream.java:699", + "void java.io.PrintStream.print(java.lang.String)+0 in PrintStream.java:863", + "void Deopt.Handle(int)+9 in Deopt.java:20", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.Handle(int)+4 in Deopt.java:15", + "void Deopt.main(java.lang.String[])+3 in Deopt.java:31", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x8d0734", + "libjvm.so+0x8d2f62", + "libjli.so+0x4a4d", + "libjli.so+0x841c", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43727, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xf5efdf", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43743, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xf5efdf", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43735, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0xbd446a", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43728, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0x7152ab", + "libjvm.so+0x601a4e", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43729, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0x76b18a", + "libjvm.so+0x76b397", + "libjvm.so+0x601a4e", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43734, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe072c", + "libjvm.so+0xcdd989", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43741, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc31d45", + "libjvm.so+0xc03e24", + "libjvm.so+0xe50735", + "libjvm.so+0x8fa0b9", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:831", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:134", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x83dbb2", + "libjvm.so+0x8f89a3", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43733, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xdcd310", + "libjvm.so+0xc1ae14", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43740, + "frames": [ + "libc-2.31.so+0xe03bf", + "libc-2.31.so+0xe6046", + "libjvm.so+0xc30b79", + "libjvm.so+0xea7aca", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43730, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0xf3623c", + "libjvm.so+0xf36e47", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43739, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe072c", + "libjvm.so+0xbf69e1", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43731, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe085a", + "libjvm.so+0x9028cb", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:243", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x83dbb2", + "libjvm.so+0x8f89a3", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43736, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe085a", + "libjvm.so+0x5e5565", + "libjvm.so+0x5e83b0", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43732, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc31b9a", + "libjvm.so+0xc0416c", + "libjvm.so+0xe50735", + "libjvm.so+0x8fa0b9", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:171", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x83dbb2", + "libjvm.so+0x8f89a3", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/java16.43768.json b/utils/coredump/testdata/amd64/java16.43768.json new file mode 100644 index 00000000..268a85a7 --- /dev/null +++ b/utils/coredump/testdata/amd64/java16.43768.json @@ -0,0 +1,337 @@ +{ + "coredump-ref": "502f91cf2e60531bd30c2506f5760b03f1442413ee6ee83af06278824c2453ca", + "threads": [ + { + "lwp": 43768, + "frames": [ + "libc-2.31.so+0x4618b", + "libc-2.31.so+0x25858", + "libjvm.so+0x244002", + "libjvm.so+0xf2fa83", + "libjvm.so+0xf3036e", + "libjvm.so+0xf303a1", + "libjvm.so+0xdcd7fd", + "libc-2.31.so+0x4620f", + "libpthread-2.31.so+0xacd4", + "libjli.so+0x904e", + "libjli.so+0x5d80", + "libjli.so+0x76d4", + "java+0x12b2", + "libc-2.31.so+0x270b2", + "java+0x135d" + ] + }, + { + "lwp": 43771, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe072c", + "libjvm.so+0x712151", + "libjvm.so+0x7132c2", + "libjvm.so+0x601a4e", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43770, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xf5efdf", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43774, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0x76b18a", + "libjvm.so+0x76b397", + "libjvm.so+0x601a4e", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43773, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0x7152ab", + "libjvm.so+0x601a4e", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43779, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe072c", + "libjvm.so+0xcdd989", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43775, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0xf3623c", + "libjvm.so+0xf36e47", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43772, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xf5efdf", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43776, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe085a", + "libjvm.so+0x9028cb", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:243", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x83dbb2", + "libjvm.so+0x8f89a3", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43780, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0xbd446a", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43777, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc31b9a", + "libjvm.so+0xc0416c", + "libjvm.so+0xe50735", + "libjvm.so+0x8fa0b9", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:171", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x83dbb2", + "libjvm.so+0x8f89a3", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43778, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xdcd310", + "libjvm.so+0xc1ae14", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43783, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0xe45ffa", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43786, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc31d45", + "libjvm.so+0xc03e24", + "libjvm.so+0xe50735", + "libjvm.so+0x8fa0b9", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:831", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:134", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x83dbb2", + "libjvm.so+0x8f89a3", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43788, + "frames": [ + "libpthread-2.31.so+0x133f4", + "libpthread-2.31.so+0x134e7", + "libjvm.so+0xcdd059", + "libjvm.so+0xf5efdf", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43769, + "frames": [ + "libjvm.so+0x59d4e4", + "libjvm.so+0x8f5c9a", + "libjvm.so+0x8c5ec4", + "libjava.so+0x168a9", + "libjava.so+0x16631", + "libjava.so+0xeb1a", + "void java.io.FileOutputStream.writeBytes(byte[], int, int, boolean)+0 in FileOutputStream.java:0", + "void java.io.FileOutputStream.write(byte[], int, int)+0 in FileOutputStream.java:347", + "void java.io.BufferedOutputStream.flushBuffer()+1 in BufferedOutputStream.java:81", + "void java.io.BufferedOutputStream.flush()+0 in BufferedOutputStream.java:142", + "void java.io.PrintStream.write(byte[], int, int)+4 in PrintStream.java:570", + "void sun.nio.cs.StreamEncoder.writeBytes()+11 in StreamEncoder.java:242", + "void sun.nio.cs.StreamEncoder.implFlushBuffer()+1 in StreamEncoder.java:321", + "void sun.nio.cs.StreamEncoder.flushBuffer()+2 in StreamEncoder.java:110", + "void java.io.OutputStreamWriter.flushBuffer()+0 in OutputStreamWriter.java:178", + "void java.io.PrintStream.writeln(java.lang.String)+5 in PrintStream.java:723", + "void java.io.PrintStream.println(java.lang.String)+1 in PrintStream.java:1028", + "void HelloWorld.main(java.lang.String[])+0 in HelloWorld.java:4", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x83c424", + "libjvm.so+0x8d0734", + "libjvm.so+0x8d2f62", + "libjli.so+0x4a4d", + "libjli.so+0x841c", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43784, + "frames": [ + "libpthread-2.31.so+0x10376", + "libjvm.so+0xc3239a", + "libjvm.so+0xbe072c", + "libjvm.so+0xbf69e1", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43785, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe072c", + "libjvm.so+0xea7974", + "libjvm.so+0xea7a58", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43782, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe085a", + "libjvm.so+0x5e5565", + "libjvm.so+0x5e83b0", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + }, + { + "lwp": 43781, + "frames": [ + "libpthread-2.31.so+0x107b1", + "libjvm.so+0xc323df", + "libjvm.so+0xbe085a", + "libjvm.so+0x5e5565", + "libjvm.so+0x5e83b0", + "libjvm.so+0xea8a5a", + "libjvm.so+0xead720", + "libjvm.so+0xc290ae", + "libpthread-2.31.so+0x9608", + "libc-2.31.so+0x122292" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/java20-helloworld.json b/utils/coredump/testdata/amd64/java20-helloworld.json new file mode 100644 index 00000000..a470033c --- /dev/null +++ b/utils/coredump/testdata/amd64/java20-helloworld.json @@ -0,0 +1,386 @@ +{ + "coredump-ref": "14b0ec7c3df287683b8ba70258b9b7abd8503e61f9b3e6ceac5b8052359f8d27", + "threads": [ + { + "lwp": 13577, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x56462", + "libjli.so+0x835e", + "libjli.so+0x59bc", + "libjli.so+0x64f8", + "java+0x1210", + "ld-musl-x86_64.so.1+0x1baac", + "java+0x1290" + ] + }, + { + "lwp": 13578, + "frames": [ + "void sun.nio.cs.StreamEncoder.implWrite(java.nio.CharBuffer)+10 in StreamEncoder.java:377", + "void sun.nio.cs.StreamEncoder.implWrite(char[], int, int)+1 in StreamEncoder.java:361", + "void sun.nio.cs.StreamEncoder.lockedWrite(char[], int, int)+7 in StreamEncoder.java:162", + "void sun.nio.cs.StreamEncoder.write(char[], int, int)+4 in StreamEncoder.java:143", + "void java.io.OutputStreamWriter.write(char[], int, int)+0 in OutputStreamWriter.java:220", + "void java.io.BufferedWriter.implFlushBuffer()+3 in BufferedWriter.java:178", + "void java.io.BufferedWriter.flushBuffer()+4 in BufferedWriter.java:163", + "void java.io.PrintStream.implWriteln(java.lang.String)+3 in PrintStream.java:848", + "void java.io.PrintStream.writeln(java.lang.String)+3 in PrintStream.java:826", + "void java.io.PrintStream.println(java.lang.String)+1 in PrintStream.java:1168", + "void HelloWorld.main(java.lang.String[])+0 in HelloWorld.java:4", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8bc159", + "libjvm.so+0x963c7b", + "libjvm.so+0x96668c", + "libjli.so+0x46c8", + "libjli.so+0x7aa8", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13579, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x57aea", + "libjvm.so+0xd3d841", + "libjvm.so+0xfc2a4a", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13580, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc9816a", + "libjvm.so+0xc4608f", + "libjvm.so+0x78c2a9", + "libjvm.so+0x642469", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13581, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x57aea", + "libjvm.so+0xd3d841", + "libjvm.so+0xfc2a4a", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13582, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc9816a", + "libjvm.so+0xc4608f", + "libjvm.so+0x792db2", + "libjvm.so+0x79315d", + "libjvm.so+0x642469", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13583, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc980dc", + "libjvm.so+0xc4608f", + "libjvm.so+0x7ec074", + "libjvm.so+0x7ec44f", + "libjvm.so+0x642469", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13584, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc980dc", + "libjvm.so+0xc4608f", + "libjvm.so+0xf9c2e4", + "libjvm.so+0xf9cd0f", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13585, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc9816a", + "libjvm.so+0xc46106", + "libjvm.so+0x9998c9", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:246", + "void java.lang.ref.Reference$ReferenceHandler.run()+3 in Reference.java:208", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8bc159", + "libjvm.so+0x8bda8b", + "libjvm.so+0x98e9b8", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13586, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc97912", + "libjvm.so+0xc6a6c4", + "libjvm.so+0xec452b", + "libjvm.so+0x9901e5", + "void java.lang.Object.wait0(long)+0 in Object.java:0", + "void java.lang.Object.wait(long)+2 in Object.java:366", + "void java.lang.Object.wait()+0 in Object.java:339", + "void java.lang.ref.NativeReferenceQueue.await()+0 in NativeReferenceQueue.java:48", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove0()+2 in ReferenceQueue.java:158", + "java.lang.ref.Reference java.lang.ref.NativeReferenceQueue.remove()+1 in NativeReferenceQueue.java:89", + "void java.lang.ref.Finalizer$FinalizerThread.run()+7 in Finalizer.java:173", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8bc159", + "libjvm.so+0x8bda8b", + "libjvm.so+0x98e9b8", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13587, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x57aea", + "libjvm.so+0xd3d841", + "libjvm.so+0xe02d7a", + "libjvm.so+0xc8203c", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13588, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc9816a", + "libjvm.so+0xc4608f", + "libjvm.so+0xd3f1ac", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13589, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc980dc", + "libjvm.so+0xc4608f", + "libjvm.so+0xc3a0ab", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13590, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc980dc", + "libjvm.so+0xc46106", + "libjvm.so+0x6250fc", + "libjvm.so+0x626c4f", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13591, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc980dc", + "libjvm.so+0xc46106", + "libjvm.so+0x6250fc", + "libjvm.so+0x626c4f", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13592, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc9816a", + "libjvm.so+0xc4608f", + "libjvm.so+0xc5cbd9", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13593, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc980dc", + "libjvm.so+0xc4608f", + "libjvm.so+0xc5c6db", + "libjvm.so+0xc5c795", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 13594, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "libjvm.so+0xc97da6", + "libjvm.so+0xf40c0c", + "void jdk.internal.misc.Unsafe.park(boolean, long)+0 in Unsafe.java:0", + "void java.util.concurrent.locks.LockSupport.parkNanos(java.lang.Object, long)+7 in LockSupport.java:269", + "boolean java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(long, java.util.concurrent.TimeUnit)+16 in AbstractQueuedSynchronizer.java:1847", + "void java.lang.ref.ReferenceQueue.await(long)+0 in ReferenceQueue.java:71", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove0(long)+4 in ReferenceQueue.java:143", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+7 in ReferenceQueue.java:218", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.runWith(java.lang.Object, java.lang.Runnable)+1 in Thread.java:1636", + "void java.lang.Thread.run()+3 in Thread.java:1623", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:186", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x8bc159", + "libjvm.so+0x8bda8b", + "libjvm.so+0x98e9b8", + "libjvm.so+0x8d207d", + "libjvm.so+0xf10b08", + "libjvm.so+0xc8dadf", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + } + ], + "modules": [ + { + "ref": "ac02a88b68c20ab5a42ad5bb8a10c1ac992e5049ed54e31ff24b62eeea589f87", + "local-path": "/usr/lib/jvm/java-20-openjdk/lib/libjava.so" + }, + { + "ref": "eb522a9e0d4d3802cd10b6961f1848e304ffd625ec72ef0d02d95d092caedc92", + "local-path": "/usr/lib/jvm/java-20-openjdk/lib/libjimage.so" + }, + { + "ref": "1dac2f829d3f9f1dc70857e47cd74968a2b2dc9b389d39dbb40d09e66a808011", + "local-path": "/usr/lib/jvm/java-20-openjdk/lib/libjli.so" + }, + { + "ref": "92444cca4a4673c8f640fd78a187be4d4e5f99d46850e667c3014197f766a3ee", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "abe4096f3cd9a8fb0defef90b0ac03ecd3033297a476ad367c51bd6d9c08f48c", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "7a9d1fdf2d2a78ca4fa6b89a5f4f9f74ae14d9c8ad7dc001c091615bc3fcaef7", + "local-path": "/usr/lib/jvm/java-20-openjdk/bin/java" + }, + { + "ref": "bd0743111d0acc90b4c1bbc2654515ff96e1c6fafcb8a60100734b1191cc26d6", + "local-path": "/usr/lib/jvm/java-20-openjdk/lib/libjsvml.so" + }, + { + "ref": "d70c8e544abfe2b059bf7e504e7cbfad7e3d0d90f0a5f0e13f0bbc20c85aea94", + "local-path": "/usr/lib/jvm/java-20-openjdk/lib/server/libjvm.so" + }, + { + "ref": "b865c43fe862c6c04fce726f7f20f8eca6a4e34c5d4b2bd271e9372168fc6bcc", + "local-path": "/lib/libz.so.1.2.13" + } + ] +} diff --git a/utils/coredump/testdata/amd64/java7.10958.json b/utils/coredump/testdata/amd64/java7.10958.json new file mode 100644 index 00000000..671e73f9 --- /dev/null +++ b/utils/coredump/testdata/amd64/java7.10958.json @@ -0,0 +1,358 @@ +{ + "coredump-ref": "00c8635bc81d002a9dbb579514b218e1e8b6c419f8b45f0bcf337aa04c5f071e", + "threads": [ + { + "lwp": 10958, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x53882", + "libjli.so+0x7cdb", + "libjli.so+0x3e75", + "libjli.so+0x4f05", + "java+0x1096", + "ld-musl-x86_64.so.1+0x1ca02", + "java+0x10b1" + ] + }, + { + "lwp": 10959, + "frames": [ + "ld-musl-x86_64.so.1+0x55352", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x58ac2", + "libjvm.so+0x42112d", + "libjava.so+0x128cb", + "libjava.so+0xedd9", + "void java.io.FileOutputStream.writeBytes(byte[], int, int, boolean)+0 in FileOutputStream.java:0", + "void java.io.FileOutputStream.write(byte[], int, int)+3 in FileOutputStream.java:345", + "void java.io.BufferedOutputStream.flushBuffer()+1 in BufferedOutputStream.java:82", + "void java.io.BufferedOutputStream.flush()+0 in BufferedOutputStream.java:140", + "void java.io.PrintStream.write(byte[], int, int)+4 in PrintStream.java:482", + "void sun.nio.cs.StreamEncoder.writeBytes()+11 in StreamEncoder.java:221", + "void sun.nio.cs.StreamEncoder.implFlushBuffer()+1 in StreamEncoder.java:291", + "void sun.nio.cs.StreamEncoder.flushBuffer()+2 in StreamEncoder.java:104", + "void java.io.OutputStreamWriter.flushBuffer()+0 in OutputStreamWriter.java:185", + "void java.io.PrintStream.newLine()+4 in PrintStream.java:546", + "void java.io.PrintStream.println(java.lang.String)+2 in PrintStream.java:807", + "void HelloWorld.main(java.lang.String[])+0 in HelloWorld.java:4", + "StubRoutines (1)+0 in :0", + "libjvm.so+0x3fa2eb", + "libjvm.so+0x3f9917", + "libjvm.so+0x405182", + "libjvm.so+0x409902", + "libjli.so+0x339a", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10962, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10969, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4e0f02", + "libjvm.so+0x41df46", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "void java.lang.Object.wait()+0 in Object.java:503", + "void java.lang.ref.Reference$ReferenceHandler.run()+8 in Reference.java:133", + "StubRoutines (1)+0 in :0", + "libjvm.so+0x3fa2eb", + "libjvm.so+0x3f9917", + "libjvm.so+0x3f9ae5", + "libjvm.so+0x3f9b3f", + "libjvm.so+0x41ce56", + "libjvm.so+0x585d07", + "libjvm.so+0x585df2", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10964, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10971, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0x4e699d", + "libjvm.so+0x4e40a6", + "libjvm.so+0x585d07", + "libjvm.so+0x585df2", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10961, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10966, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10967, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10960, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10968, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb9c3", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x5aabaf", + "libjvm.so+0x5aaee4", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10965, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10974, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x531c6a", + "libjvm.so+0x585d07", + "libjvm.so+0x585df2", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10975, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb9c3", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x581262", + "libjvm.so+0x581308", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10972, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce6b2", + "libjvm.so+0x319005", + "libjvm.so+0x31b846", + "libjvm.so+0x585d07", + "libjvm.so+0x585df2", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10973, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce6b2", + "libjvm.so+0x319005", + "libjvm.so+0x31b846", + "libjvm.so+0x585d07", + "libjvm.so+0x585df2", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10970, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4e0f02", + "libjvm.so+0x41df46", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+7 in ReferenceQueue.java:135", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:151", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:209", + "StubRoutines (1)+0 in :0", + "libjvm.so+0x3fa2eb", + "libjvm.so+0x3f9917", + "libjvm.so+0x3f9ae5", + "libjvm.so+0x3f9b3f", + "libjvm.so+0x41ce56", + "libjvm.so+0x585d07", + "libjvm.so+0x585df2", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10963, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x4eb7de", + "libjvm.so+0x4cde32", + "libjvm.so+0x4ce303", + "libjvm.so+0x4ce66b", + "libjvm.so+0x3aba72", + "libjvm.so+0x3acab8", + "libjvm.so+0x4e8f1e", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/java8.10171.json b/utils/coredump/testdata/amd64/java8.10171.json new file mode 100644 index 00000000..8adeb013 --- /dev/null +++ b/utils/coredump/testdata/amd64/java8.10171.json @@ -0,0 +1,384 @@ +{ + "coredump-ref": "3836a6042df0507db4a8a85d78e00a555a34b05dfc4c3451f1a58ee41ce3353e", + "threads": [ + { + "lwp": 10171, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x53882", + "libjli.so+0x7ddd", + "libjli.so+0x49af", + "libjli.so+0x5bb4", + "java+0x10a0", + "ld-musl-x86_64.so.1+0x1ca02", + "java+0x10bb" + ] + }, + { + "lwp": 10172, + "frames": [ + "ld-musl-x86_64.so.1+0x55352", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x58ac2", + "libjava.so+0x197ef", + "libjava.so+0x19280", + "libjava.so+0xf695", + "void java.io.FileOutputStream.writeBytes(byte[], int, int, boolean)+0 in FileOutputStream.java:0", + "void java.io.FileOutputStream.write(byte[], int, int)+0 in FileOutputStream.java:326", + "void java.io.BufferedOutputStream.flushBuffer()+1 in BufferedOutputStream.java:82", + "void java.io.BufferedOutputStream.flush()+0 in BufferedOutputStream.java:140", + "void java.io.PrintStream.write(byte[], int, int)+4 in PrintStream.java:482", + "void sun.nio.cs.StreamEncoder.writeBytes()+11 in StreamEncoder.java:221", + "void sun.nio.cs.StreamEncoder.implFlushBuffer()+1 in StreamEncoder.java:291", + "void sun.nio.cs.StreamEncoder.flushBuffer()+2 in StreamEncoder.java:104", + "void java.io.OutputStreamWriter.flushBuffer()+0 in OutputStreamWriter.java:185", + "void java.io.PrintStream.newLine()+4 in PrintStream.java:546", + "void java.io.PrintStream.println(java.lang.String)+2 in PrintStream.java:807", + "void HelloWorld.main(java.lang.String[])+0 in HelloWorld.java:4", + "StubRoutines (1)+0 in :0", + "libjvm.so+0x42c7e0", + "libjvm.so+0x45bddf", + "libjvm.so+0x45c0a5", + "libjli.so+0x333f", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10186, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a7df", + "libjvm.so+0x548411", + "libjvm.so+0x5487df", + "libjvm.so+0x33870d", + "libjvm.so+0x33a7b9", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10174, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10190, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a7df", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x60827e", + "libjvm.so+0x608335", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10173, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10188, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a7df", + "libjvm.so+0x548411", + "libjvm.so+0x5487df", + "libjvm.so+0x33870d", + "libjvm.so+0x33a7b9", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10179, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10177, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10184, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x54ea5", + "libjvm.so+0x565836", + "libjvm.so+0x561496", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10180, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10176, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10182, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x55bb63", + "libjvm.so+0x472582", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "void java.lang.Object.wait()+0 in Object.java:502", + "boolean java.lang.ref.Reference.tryHandlePending(boolean)+13 in Reference.java:191", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:153", + "StubRoutines (1)+0 in :0", + "libjvm.so+0x42c7e0", + "libjvm.so+0x42bcdd", + "libjvm.so+0x42bd62", + "libjvm.so+0x4700f8", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10175, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10178, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x3d8694", + "libjvm.so+0x3d9720", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10187, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a7df", + "libjvm.so+0x548411", + "libjvm.so+0x5487df", + "libjvm.so+0x33870d", + "libjvm.so+0x33a7b9", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10189, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x547f40", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x5b58ab", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10183, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a5fa", + "libjvm.so+0x55bb63", + "libjvm.so+0x472582", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:144", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:165", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:216", + "StubRoutines (1)+0 in :0", + "libjvm.so+0x42c7e0", + "libjvm.so+0x42bcdd", + "libjvm.so+0x42bd62", + "libjvm.so+0x4700f8", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10185, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a7df", + "libjvm.so+0x548411", + "libjvm.so+0x5487df", + "libjvm.so+0x33870d", + "libjvm.so+0x33a7b9", + "libjvm.so+0x60cff7", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + }, + { + "lwp": 10181, + "frames": [ + "ld-musl-x86_64.so.1+0x55354", + "ld-musl-x86_64.so.1+0x5266d", + "ld-musl-x86_64.so.1+0x51c6a", + "ld-musl-x86_64.so.1+0x52a77", + "libjvm.so+0x56a7df", + "libjvm.so+0x548411", + "libjvm.so+0x548789", + "libjvm.so+0x635187", + "libjvm.so+0x635512", + "libjvm.so+0x567e30", + "ld-musl-x86_64.so.1+0x53161", + "ld-musl-x86_64.so.1+0x55320" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/musl-signalframe.json b/utils/coredump/testdata/amd64/musl-signalframe.json new file mode 100644 index 00000000..1df2dac8 --- /dev/null +++ b/utils/coredump/testdata/amd64/musl-signalframe.json @@ -0,0 +1,40 @@ +{ + "coredump-ref": "69d481450a189176db6d7d23bc420a5e50adff8e75c432d5f7b68547e0908c6c", + "threads": [ + { + "lwp": 5576, + "frames": [ + "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", + "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+0x520ed", + "ld-musl-x86_64.so.1+0x561dd", + "ld-musl-x86_64.so.1+0x56546", + "ld-musl-x86_64.so.1+0x5846a", + "sig+0x11f8", + "ld-musl-x86_64.so.1+0x1b87f", + "sig+0x1065", + "" + ] + } + ], + "modules": [ + { + "ref": "98b7650549c4fc55448ecf25df03d4f93f3aa2ed4b23861b33b9bab09304a8fa", + "local-path": "/home/tteras/work/elastic/java/sig" + }, + { + "ref": "c3f0829a87fce28a23d4b5edea62b5513eb72fbee4a95864796e86a7737c6b9b", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "c10979df29fdec45399700b27b9fa4615cbffa3523f486c80f471efa6a37170a", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + } + ] +} diff --git a/utils/coredump/testdata/amd64/node1610.26681.json b/utils/coredump/testdata/amd64/node1610.26681.json new file mode 100644 index 00000000..076c3a70 --- /dev/null +++ b/utils/coredump/testdata/amd64/node1610.26681.json @@ -0,0 +1,127 @@ +{ + "coredump-ref": "c74393e19c4d0d51d1f0a2bf1f2046b9e2cbe45952d8f42087109ab5c973613e", + "threads": [ + { + "lwp": 26681, + "frames": [ + "node+0x9e7c50", + "node+0x7ec150", + "V8::ExitFrame+0 in :0", + "writeSync+15 in node:fs:873", + "SyncWriteStream._write+0 in node:internal/fs/sync_write_stream:0", + "writeOrBuffer+24 in node:internal/streams/writable:389", + "_write+47 in node:internal/streams/writable:330", + "Writable.write+1 in node:internal/streams/writable:334", + "value+28 in node:internal/console/constructor:289", + "log+1 in node:internal/console/constructor:363", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xb8ee01", + "node+0xb8fad4", + "node+0xa0650f", + "node+0x905af6", + "V8::ExitFrame+0 in :0", + "+11 in /home/tteras/optimyze/node/hello.js:12", + "Module._compile+46 in node:internal/modules/cjs/loader:1101", + "Module._extensions..js+43 in node:internal/modules/cjs/loader:1153", + "Module.load+12 in node:internal/modules/cjs/loader:981", + "Module._load+65 in node:internal/modules/cjs/loader:822", + "executeUserEntryPoint+7 in node:internal/modules/run_main:79", + "+16 in node:internal/main/run_main_module:17", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xb8ee01", + "node+0xb8fad4", + "node+0xa0650f", + "node+0x798836", + "node+0x798b22", + "node+0x799f39", + "node+0x709d93", + "node+0x8271d9", + "node+0x82761a", + "node+0x79c575", + "ld-musl-x86_64.so.1+0x1ca02", + "node+0x7034a3" + ] + }, + { + "lwp": 26683, + "frames": [ + "ld-musl-x86_64.so.1+0x55413", + "ld-musl-x86_64.so.1+0x5272c", + "ld-musl-x86_64.so.1+0x51d29", + "ld-musl-x86_64.so.1+0x52b36", + "libuv.so.1.0.0+0x185fa", + "node+0x8571da", + "ld-musl-x86_64.so.1+0x53220", + "ld-musl-x86_64.so.1+0x553df" + ] + }, + { + "lwp": 26684, + "frames": [ + "ld-musl-x86_64.so.1+0x55413", + "ld-musl-x86_64.so.1+0x5272c", + "ld-musl-x86_64.so.1+0x51d29", + "ld-musl-x86_64.so.1+0x52b36", + "libuv.so.1.0.0+0x185fa", + "node+0x8571da", + "ld-musl-x86_64.so.1+0x53220", + "ld-musl-x86_64.so.1+0x553df" + ] + }, + { + "lwp": 26685, + "frames": [ + "ld-musl-x86_64.so.1+0x55413", + "ld-musl-x86_64.so.1+0x5272c", + "ld-musl-x86_64.so.1+0x51d29", + "ld-musl-x86_64.so.1+0x52b36", + "libuv.so.1.0.0+0x185fa", + "node+0x8571da", + "ld-musl-x86_64.so.1+0x53220", + "ld-musl-x86_64.so.1+0x553df" + ] + }, + { + "lwp": 26682, + "frames": [ + "ld-musl-x86_64.so.1+0x55413", + "ld-musl-x86_64.so.1+0x5272c", + "ld-musl-x86_64.so.1+0x1fdbc", + "libuv.so.1.0.0+0x1cdbf", + "libuv.so.1.0.0+0xd8ee", + "node+0x85bab9", + "ld-musl-x86_64.so.1+0x53220", + "ld-musl-x86_64.so.1+0x553df" + ] + }, + { + "lwp": 26687, + "frames": [ + "ld-musl-x86_64.so.1+0x55413", + "ld-musl-x86_64.so.1+0x5272c", + "ld-musl-x86_64.so.1+0x51d29", + "ld-musl-x86_64.so.1+0x54f64", + "libuv.so.1.0.0+0x184cc", + "node+0x8f8de4", + "ld-musl-x86_64.so.1+0x53220", + "ld-musl-x86_64.so.1+0x553df" + ] + }, + { + "lwp": 26686, + "frames": [ + "ld-musl-x86_64.so.1+0x55413", + "ld-musl-x86_64.so.1+0x5272c", + "ld-musl-x86_64.so.1+0x51d29", + "ld-musl-x86_64.so.1+0x52b36", + "libuv.so.1.0.0+0x185fa", + "node+0x8571da", + "ld-musl-x86_64.so.1+0x53220", + "ld-musl-x86_64.so.1+0x553df" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/node1617-inlining.json b/utils/coredump/testdata/amd64/node1617-inlining.json new file mode 100644 index 00000000..20726eb9 --- /dev/null +++ b/utils/coredump/testdata/amd64/node1617-inlining.json @@ -0,0 +1,191 @@ +{ + "coredump-ref": "e724cd571e6d1da4c1e7d97f826c01c27f72532335b1fb77108be532639d11d6", + "threads": [ + { + "lwp": 13591, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "node+0x1193d48", + "node+0x771dca", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 13589, + "frames": [ + "ld-musl-x86_64.so.1+0x566d8", + "node+0x119812a", + "node+0x119748e", + "node+0x1185d76", + "node+0x7750a7", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 13594, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x55005", + "node+0x1193bd1", + "node+0x7f7920", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 13588, + "frames": [ + "node+0xcf88b2", + "node+0xcffaae", + "node+0xcffdd1", + "node+0xcb985a", + "node+0xceb17a", + "node+0xcb6a6d", + "node+0x958702", + "V8::BuiltinExitFrame+0 in :0", + "trace+5 in node:internal/console/constructor:426", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xa1aab4", + "node+0xa1b8e1", + "node+0x8ee8bb", + "node+0x8055bf", + "V8::ExitFrame+0 in :0", + "add+1 in /home/tteras/optimyze/node/test.js:3", + "add3+1 in /home/tteras/optimyze/node/test.js:8", + "test+1 in /home/tteras/optimyze/node/test.js:12", + "submain+2 in /home/tteras/optimyze/node/test.js:17", + "main+2 in /home/tteras/optimyze/node/test.js:23", + "+26 in /home/tteras/optimyze/node/test.js:27", + "Module._compile+46 in node:internal/modules/cjs/loader:1126", + "Module._extensions..js+45 in node:internal/modules/cjs/loader:1180", + "Module.load+12 in node:internal/modules/cjs/loader:1004", + "Module._load+68 in node:internal/modules/cjs/loader:839", + "executeUserEntryPoint+0 in node:internal/modules/run_main:0", + "+0 in node:internal/main/run_main_module:0", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xa1aab4", + "node+0xa1b8e1", + "node+0x8ee8bb", + "node+0x6c8695", + "node+0x6c88bc", + "node+0x6ca399", + "node+0x644d72", + "node+0x746acd", + "node+0x746eef", + "node+0x6cc146", + "ld-musl-x86_64.so.1+0x1ca21", + "node+0x618c1c" + ] + }, + { + "lwp": 13590, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "node+0x1193d48", + "node+0x771dca", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 13592, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "node+0x1193d48", + "node+0x771dca", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 13593, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "node+0x1193d48", + "node+0x771dca", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + } + ], + "modules": [ + { + "ref": "5ab1ef5271e278d73e0bf67f8ffd5316ebdd7f65ba80d78e8ab73d2a081abe0a", + "local-path": "/usr/lib/libgcc_s.so.1" + }, + { + "ref": "5437ef6d210060ffd1155a4fbb3efe900e81588f0bb6a7e06935ec9bf27668b4", + "local-path": "/usr/lib/libstdc++.so.6.0.30" + }, + { + "ref": "9bdf749ca620aac88e30575deac05d6f076ed01ebfcd4c59a461c9b08192eabd", + "local-path": "/usr/lib/libcares.so.2.5.1" + }, + { + "ref": "ed4af2df45ae11c1427cdedb3e8709689be255f82fe238086fef9525a07db39c", + "local-path": "/lib/libz.so.1.2.12" + }, + { + "ref": "0afad06589227d52a9be225f7c29b1a1ebc045e6b19d6be4d5d0484e2629e882", + "local-path": "/usr/bin/node" + }, + { + "ref": "cd3409849cb7fb1d938cb3d52a39951d2d3084af994b7a92246affe19ab2a5e4", + "local-path": "/usr/lib/libicui18n.so.71.1" + }, + { + "ref": "465eee61ae9960b541c86df05007b18abb2758aaa539790d4e247d9ca6c2f1f2", + "local-path": "/lib/libcrypto.so.3" + }, + { + "ref": "703e21e00df635be9406c653ab153ea6cc31d16b24eedbf8147f589da6355917", + "local-path": "/usr/lib/libbrotlienc.so.1.0.9" + }, + { + "ref": "77d4e17277f51885a677d16771620f0a8fa5696f5e05db3e6cc89c2d4df53f94", + "local-path": "/usr/lib/libbrotlidec.so.1.0.9" + }, + { + "ref": "363684f4c2cc049e17e9e784d6369b4f590c11e08d69538e01a2d90dc0914ef5", + "local-path": "/usr/lib/libbrotlicommon.so.1.0.9" + }, + { + "ref": "a3a410820b3d773d81a75a7dcc9d2346a5dad03e92cf82b35c92c2c13098aeb2", + "local-path": "/usr/lib/libnghttp2.so.14.23.0" + }, + { + "ref": "2a973a89a5d64b0fe31c1bce2339d3774b683c526513b7b0bbaa487d4d0c3d3b", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "8ec76ae6d3acc71c81eaa12797f71d02a2f42c39b15d310c4e3619f69d11e6ab", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "1b8ef46271445700050eb307b887fc73cc1b687ed43e83151d2c696cbdf3e054", + "local-path": "/usr/lib/libicuuc.so.71.1" + }, + { + "ref": "d69e8d7e2f8570f8b736c6061f9dc0bfe969733a17f3f5518ad33890dc7928c4", + "local-path": "/lib/libssl.so.3" + } + ] +} diff --git a/utils/coredump/testdata/amd64/node1815-baseline.json b/utils/coredump/testdata/amd64/node1815-baseline.json new file mode 100644 index 00000000..dfcf2167 --- /dev/null +++ b/utils/coredump/testdata/amd64/node1815-baseline.json @@ -0,0 +1,197 @@ +{ + "coredump-ref": "f9f50c571dbc44589186c3bdf8f515c2f33451da023e28879f0f289ba026a9c1", + "threads": [ + { + "lwp": 7564, + "frames": [ + "V8::ExitFrame+0 in :0", + "getColorDepth+0 in node:internal/tty:0", + "value+5 in node:internal/console/constructor:320", + "value+1 in node:internal/console/constructor:347", + "warn+1 in node:internal/console/constructor:382", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0x9bfced", + "node+0x9c0cb1", + "node+0x877853", + "node+0x7906bf", + "V8::ExitFrame+0 in :0", + "trace+6 in node:internal/console/constructor:428", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0x9bfced", + "node+0x9c0cb1", + "node+0x877853", + "node+0x7906bf", + "V8::ExitFrame+0 in :0", + "add+1 in /home/tteras/work/elastic/node/test.js:3", + "add3+1 in /home/tteras/work/elastic/node/test.js:8", + "test+1 in /home/tteras/work/elastic/node/test.js:12", + "submain+1 in /home/tteras/work/elastic/node/test.js:16", + "main+2 in /home/tteras/work/elastic/node/test.js:23", + "+26 in /home/tteras/work/elastic/node/test.js:27", + "Module._compile+46 in node:internal/modules/cjs/loader:1254", + "Module._extensions..js+45 in node:internal/modules/cjs/loader:1308", + "Module.load+12 in node:internal/modules/cjs/loader:1117", + "Module._load+72 in node:internal/modules/cjs/loader:958", + "executeUserEntryPoint+0 in node:internal/modules/run_main:0", + "+0 in node:internal/main/run_main_module:0", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0x9bfced", + "node+0x9c0cb1", + "node+0x877853", + "node+0x6f04e6", + "node+0x62e6a9", + "node+0x6308c8", + "node+0x5b3992", + "node+0x6b8b25", + "node+0x6b98ea", + "node+0x62fd8f", + "node+0x632f82", + "ld-musl-x86_64.so.1+0x1baac", + "node+0x58197c" + ] + }, + { + "lwp": 7565, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x1eed3", + "node+0x11dfb04", + "node+0x11ce153", + "node+0x6e3827", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 7566, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "node+0x11dc118", + "node+0x6e05ba", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 7567, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "node+0x11dc118", + "node+0x6e05ba", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 7568, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "node+0x11dc118", + "node+0x6e05ba", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 7569, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x55514", + "node+0x11dc118", + "node+0x6e05ba", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + }, + { + "lwp": 7570, + "frames": [ + "ld-musl-x86_64.so.1+0x57f81", + "ld-musl-x86_64.so.1+0x5510b", + "ld-musl-x86_64.so.1+0x546d6", + "ld-musl-x86_64.so.1+0x57aea", + "node+0x11dbfa1", + "node+0x781bc1", + "ld-musl-x86_64.so.1+0x55bdf", + "ld-musl-x86_64.so.1+0x57f4d" + ] + } + ], + "modules": [ + { + "ref": "2f15e5cf60c63c296e982052e49d9c44d6f52431eb41537ba4f71f842d518ded", + "local-path": "/usr/lib/libstdc++.so.6.0.30" + }, + { + "ref": "e54a14f63a00fa869bc24002d79c0d53215a2e456fb6bccac7379758749c7a1e", + "local-path": "/lib/libssl.so.3" + }, + { + "ref": "c128a02af22c672acc5a9c0106b24e9e12cddf4038f3519aca79ab3e53ac2f19", + "local-path": "/usr/lib/libcares.so.2.6.0" + }, + { + "ref": "6e1d5d052d55bbff81ecccc8a271694bd9c2f78bee5ef7a50f289b2f86b07b1e", + "local-path": "/usr/lib/libicui18n.so.72.1" + }, + { + "ref": "3ef4635dca6d0269348caaecffe325faa4b67722d4e9cbf49331187c48bc1517", + "local-path": "/lib/libcrypto.so.3" + }, + { + "ref": "1720409815423c3a8ff923c07118e635fc414f2aff0ba92bd7ccdcfe87016dfb", + "local-path": "/usr/lib/libbrotlicommon.so.1.0.9" + }, + { + "ref": "2ff2fe1b0f4a342226fa6012a5ea509769af19ad0b325a152173226023c525cd", + "local-path": "/usr/lib/libnghttp2.so.14.24.1" + }, + { + "ref": "9bad037e78fe4ef35bf0a97fe09ab53c4a7241155bc31372f6dca6463efed34a", + "local-path": "/usr/bin/node" + }, + { + "ref": "fd2fd9f296ea16052681d3192e7356f28e2fc916c5b5220610b86af03688ef17", + "local-path": "/usr/lib/libgcc_s.so.1" + }, + { + "ref": "b865c43fe862c6c04fce726f7f20f8eca6a4e34c5d4b2bd271e9372168fc6bcc", + "local-path": "/lib/libz.so.1.2.13" + }, + { + "ref": "86caea8c44fcd368a9c998f3e1e385c4346c917dda5e94c5920481d6ed8d6c8b", + "local-path": "/usr/lib/libicuuc.so.72.1" + }, + { + "ref": "cc912e63cebce01c23c27e038e5ede1bcb8ea3ee9b460c61287f718c0927f159", + "local-path": "/usr/lib/libbrotlienc.so.1.0.9" + }, + { + "ref": "6f44493fa40580fe818ddd7ae810ac9fd981fb2bdf0d81dafdabf9bea4e05dff", + "local-path": "/usr/lib/libbrotlidec.so.1.0.9" + }, + { + "ref": "92444cca4a4673c8f640fd78a187be4d4e5f99d46850e667c3014197f766a3ee", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "abe4096f3cd9a8fb0defef90b0ac03ecd3033297a476ad367c51bd6d9c08f48c", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + } + ] +} diff --git a/utils/coredump/testdata/amd64/node1816-async.json b/utils/coredump/testdata/amd64/node1816-async.json new file mode 100644 index 00000000..a80cb63e --- /dev/null +++ b/utils/coredump/testdata/amd64/node1816-async.json @@ -0,0 +1,200 @@ +{ + "coredump-ref": "071e1a04783da57c0a534e6502c519d7202141dad947a46b99a9eb31dd4174e2", + "threads": [ + { + "lwp": 8255, + "frames": [ + "node+0x9a939f", + "node+0x9ad13e", + "node+0x9ad4fd", + "node+0x9dfbc1", + "node+0x9fa065", + "node+0x9fa5ce", + "node+0x9fdd6f", + "node+0x8f569c", + "V8::BuiltinExitFrame+0 in :0", + "trace+5 in node:internal/console/constructor:427", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0x9d5a90", + "node+0x9d6a9c", + "node+0x8884b6", + "node+0x79c955", + "V8::ExitFrame+0 in :0", + "+2 in /home/tteras/work/elastic/node/async.js:29", + "Promise+0 in :0", + "V8::ConstructFrame+0 in :0", + "V8::StubFrame+0 in :0", + "serveCustomer+1 in /home/tteras/work/elastic/node/async.js:27", + "runRestaurant+4 in /home/tteras/work/elastic/node/async.js:43", + "+0 in :0", + "V8::StubFrame+0 in :0", + "V8::StubFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0x9d5bde", + "node+0x9d6d1f", + "node+0x9d6eb4", + "node+0xa0affe", + "node+0xa0b34d", + "node+0x8cd006", + "node+0x8ce178", + "V8::BuiltinExitFrame+0 in :0", + "processTicksAndRejections+29 in node:internal/process/task_queues:96", + "runNextTicks+6 in node:internal/process/task_queues:64", + "listOnTimeout+21 in node:internal/timers:538", + "processTimers+6 in node:internal/timers:503", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0x9d5a90", + "node+0x9d6a9c", + "node+0x8884b6", + "node+0x602d09", + "node+0x1205e54", + "node+0x1209de0", + "node+0x5b7500", + "node+0x6c51f6", + "node+0x6c5fcb", + "node+0x638d16", + "node+0x63c01b", + "ld-musl-x86_64.so.1+0x1baac", + "node+0x5879d9" + ] + }, + { + "lwp": 8256, + "frames": [ + "ld-musl-x86_64.so.1+0x57fac", + "ld-musl-x86_64.so.1+0x55136", + "ld-musl-x86_64.so.1+0x1eed3", + "node+0x121c3ba", + "node+0x1209e2a", + "node+0x6f0a1c", + "ld-musl-x86_64.so.1+0x55c0a", + "ld-musl-x86_64.so.1+0x57f78" + ] + }, + { + "lwp": 8257, + "frames": [ + "ld-musl-x86_64.so.1+0x57fac", + "ld-musl-x86_64.so.1+0x55136", + "ld-musl-x86_64.so.1+0x54701", + "ld-musl-x86_64.so.1+0x5553f", + "node+0x12186c8", + "node+0x6ed672", + "ld-musl-x86_64.so.1+0x55c0a", + "ld-musl-x86_64.so.1+0x57f78" + ] + }, + { + "lwp": 8258, + "frames": [ + "ld-musl-x86_64.so.1+0x57fac", + "ld-musl-x86_64.so.1+0x55136", + "ld-musl-x86_64.so.1+0x54701", + "ld-musl-x86_64.so.1+0x5553f", + "node+0x12186c8", + "node+0x6ed672", + "ld-musl-x86_64.so.1+0x55c0a", + "ld-musl-x86_64.so.1+0x57f78" + ] + }, + { + "lwp": 8259, + "frames": [ + "ld-musl-x86_64.so.1+0x57fac", + "ld-musl-x86_64.so.1+0x55136", + "ld-musl-x86_64.so.1+0x54701", + "ld-musl-x86_64.so.1+0x5553f", + "node+0x12186c8", + "node+0x6ed672", + "ld-musl-x86_64.so.1+0x55c0a", + "ld-musl-x86_64.so.1+0x57f78" + ] + }, + { + "lwp": 8260, + "frames": [ + "ld-musl-x86_64.so.1+0x57fac", + "ld-musl-x86_64.so.1+0x55136", + "ld-musl-x86_64.so.1+0x54701", + "ld-musl-x86_64.so.1+0x5553f", + "node+0x12186c8", + "node+0x6ed672", + "ld-musl-x86_64.so.1+0x55c0a", + "ld-musl-x86_64.so.1+0x57f78" + ] + }, + { + "lwp": 8261, + "frames": [ + "ld-musl-x86_64.so.1+0x57fac", + "ld-musl-x86_64.so.1+0x55136", + "ld-musl-x86_64.so.1+0x54701", + "ld-musl-x86_64.so.1+0x57b15", + "node+0x1218551", + "node+0x78d161", + "ld-musl-x86_64.so.1+0x55c0a", + "ld-musl-x86_64.so.1+0x57f78" + ] + } + ], + "modules": [ + { + "ref": "c6b3288ba48945a21ede7ccf6f7a257d41bacf2c061236726ff9b5def383a766", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "d73e165c5a4c95589dd8e86e7cc131e471eea28b252011b750ab87fbc9739016", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "c91aeb888636f562fe74f9133abd4eac5259c8ab43fc7d21e3dde5a337fba8c6", + "local-path": "/usr/bin/node" + }, + { + "ref": "2f15e5cf60c63c296e982052e49d9c44d6f52431eb41537ba4f71f842d518ded", + "local-path": "/usr/lib/libstdc++.so.6.0.30" + }, + { + "ref": "995cc8e85cd6b9769a83efdab853b4d05ba86ed1bdb39b569f7d63f97b5e27ab", + "local-path": "/usr/lib/libbrotlicommon.so.1.0.9" + }, + { + "ref": "fd2fd9f296ea16052681d3192e7356f28e2fc916c5b5220610b86af03688ef17", + "local-path": "/usr/lib/libgcc_s.so.1" + }, + { + "ref": "b865c43fe862c6c04fce726f7f20f8eca6a4e34c5d4b2bd271e9372168fc6bcc", + "local-path": "/lib/libz.so.1.2.13" + }, + { + "ref": "d8aaa6af79c0a0c4d0f4687422e4cc330f04e480535f380977c7be553a8e36ef", + "local-path": "/usr/lib/libicui18n.so.73.1" + }, + { + "ref": "4b4c10a7d32f55bb68d671d3ac53dc29442d7326863d1a0fefb877e585ebf71d", + "local-path": "/usr/lib/libcares.so.2.6.0" + }, + { + "ref": "226cb34f3cf708521c167f998c95fbc4ce7cd92c30e3ce0553dbc7f5cdd29171", + "local-path": "/usr/lib/libbrotlienc.so.1.0.9" + }, + { + "ref": "24d5c76824bdc72821678492e6350830d2fca6069d19cbe589ef4f0187781b0e", + "local-path": "/usr/lib/libicuuc.so.73.1" + }, + { + "ref": "9ec9a27c455af033247e18781e33907a84446281f0b51f4b9b3b1b43cb01dd56", + "local-path": "/lib/libcrypto.so.3" + }, + { + "ref": "72bbcf719bd93c00701f9b86e240f86d771b49f3ab00f2436275e58d11940292", + "local-path": "/lib/libssl.so.3" + }, + { + "ref": "17d79c05629b778fbddeba441356d8dafec8c6c02d44454e18f4a399f361b1b4", + "local-path": "/usr/lib/libbrotlidec.so.1.0.9" + } + ] +} diff --git a/utils/coredump/testdata/amd64/node189-inlining.json b/utils/coredump/testdata/amd64/node189-inlining.json new file mode 100644 index 00000000..e867cc60 --- /dev/null +++ b/utils/coredump/testdata/amd64/node189-inlining.json @@ -0,0 +1,198 @@ +{ + "coredump-ref": "136df05d67bcb15743e7107d0cbd9078a168e42b62efe2017fc0e7bbb2c0479f", + "threads": [ + { + "lwp": 17272, + "frames": [ + "node+0xefb26a", + "node+0xe70c1d", + "node+0xb54ea0", + "node+0xb5ac91", + "node+0xb5c255", + "node+0x9eb4f7", + "node+0xefd2a7", + "node+0x10a7bc2", + "V8::ExitFrame+0 in :0", + "trace+6 in node:internal/console/constructor:427", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xb1dd4d", + "node+0xb1ef67", + "node+0x99f5ad", + "node+0x897b4a", + "V8::ExitFrame+0 in :0", + "add+1 in /home/tteras/optimyze/node/test.js:3", + "add3+1 in /home/tteras/optimyze/node/test.js:8", + "test+1 in /home/tteras/optimyze/node/test.js:12", + "submain+2 in /home/tteras/optimyze/node/test.js:17", + "main+2 in /home/tteras/optimyze/node/test.js:23", + "+26 in /home/tteras/optimyze/node/test.js:27", + "Module._compile+46 in node:internal/modules/cjs/loader:1119", + "Module._extensions..js+45 in node:internal/modules/cjs/loader:1173", + "Module.load+12 in node:internal/modules/cjs/loader:997", + "Module._load+68 in node:internal/modules/cjs/loader:838", + "executeUserEntryPoint+0 in node:internal/modules/run_main:0", + "+0 in node:internal/main/run_main_module:0", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xb1dd4d", + "node+0xb1ef67", + "node+0x99f5ad", + "node+0x72803d", + "node+0x728146", + "node+0x729cc6", + "node+0x69c422", + "node+0x7ba27d", + "node+0x7ba6af", + "node+0x72928f", + "node+0x72c764", + "ld-musl-x86_64.so.1+0x1ca21", + "node+0x66659c" + ] + }, + { + "lwp": 17276, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "libuv.so.1.0.0+0x1aeb4", + "node+0x7e45aa", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 17275, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "libuv.so.1.0.0+0x1aeb4", + "node+0x7e45aa", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 17273, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x1fddb", + "libuv.so.1.0.0+0x1f5f2", + "libuv.so.1.0.0+0x1021b", + "node+0x7e93c7", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 17278, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x55005", + "libuv.so.1.0.0+0x1ad86", + "node+0x887560", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 17277, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "libuv.so.1.0.0+0x1aeb4", + "node+0x7e45aa", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + }, + { + "lwp": 17274, + "frames": [ + "ld-musl-x86_64.so.1+0x554a3", + "ld-musl-x86_64.so.1+0x526f9", + "ld-musl-x86_64.so.1+0x51cf4", + "ld-musl-x86_64.so.1+0x52b03", + "libuv.so.1.0.0+0x1aeb4", + "node+0x7e45aa", + "ld-musl-x86_64.so.1+0x531f4", + "ld-musl-x86_64.so.1+0x5546f" + ] + } + ], + "modules": [ + { + "ref": "5ab1ef5271e278d73e0bf67f8ffd5316ebdd7f65ba80d78e8ab73d2a081abe0a", + "local-path": "/usr/lib/libgcc_s.so.1" + }, + { + "ref": "5437ef6d210060ffd1155a4fbb3efe900e81588f0bb6a7e06935ec9bf27668b4", + "local-path": "/usr/lib/libstdc++.so.6.0.30" + }, + { + "ref": "9cbc38e234cde3798aca950f3f63880e162383aefe376b60bad87915802fd41f", + "local-path": "/usr/lib/libuv.so.1.0.0" + }, + { + "ref": "1b8ef46271445700050eb307b887fc73cc1b687ed43e83151d2c696cbdf3e054", + "local-path": "/usr/lib/libicuuc.so.71.1" + }, + { + "ref": "a3a410820b3d773d81a75a7dcc9d2346a5dad03e92cf82b35c92c2c13098aeb2", + "local-path": "/usr/lib/libnghttp2.so.14.23.0" + }, + { + "ref": "9bdf749ca620aac88e30575deac05d6f076ed01ebfcd4c59a461c9b08192eabd", + "local-path": "/usr/lib/libcares.so.2.5.1" + }, + { + "ref": "465eee61ae9960b541c86df05007b18abb2758aaa539790d4e247d9ca6c2f1f2", + "local-path": "/lib/libcrypto.so.3" + }, + { + "ref": "ed4af2df45ae11c1427cdedb3e8709689be255f82fe238086fef9525a07db39c", + "local-path": "/lib/libz.so.1.2.12" + }, + { + "ref": "cd3409849cb7fb1d938cb3d52a39951d2d3084af994b7a92246affe19ab2a5e4", + "local-path": "/usr/lib/libicui18n.so.71.1" + }, + { + "ref": "d69e8d7e2f8570f8b736c6061f9dc0bfe969733a17f3f5518ad33890dc7928c4", + "local-path": "/lib/libssl.so.3" + }, + { + "ref": "703e21e00df635be9406c653ab153ea6cc31d16b24eedbf8147f589da6355917", + "local-path": "/usr/lib/libbrotlienc.so.1.0.9" + }, + { + "ref": "77d4e17277f51885a677d16771620f0a8fa5696f5e05db3e6cc89c2d4df53f94", + "local-path": "/usr/lib/libbrotlidec.so.1.0.9" + }, + { + "ref": "2a973a89a5d64b0fe31c1bce2339d3774b683c526513b7b0bbaa487d4d0c3d3b", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "8ec76ae6d3acc71c81eaa12797f71d02a2f42c39b15d310c4e3619f69d11e6ab", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "877aef4b82e3c6dd8b54b18ed742f97a4d7c37229b5ab50a50eedac20456b404", + "local-path": "/usr/bin/node" + }, + { + "ref": "363684f4c2cc049e17e9e784d6369b4f590c11e08d69538e01a2d90dc0914ef5", + "local-path": "/usr/lib/libbrotlicommon.so.1.0.9" + } + ] +} diff --git a/utils/coredump/testdata/amd64/node211.json b/utils/coredump/testdata/amd64/node211.json new file mode 100644 index 00000000..35eaa1cb --- /dev/null +++ b/utils/coredump/testdata/amd64/node211.json @@ -0,0 +1,232 @@ +{ + "coredump-ref": "bbe8fe6b51893ce3e0f9b0b9f26adb399c5056f7e0e315a53efcdf805e0d8ff8", + "threads": [ + { + "lwp": 18947, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x62943", + "libuv.so.1.0.0+0x1abce", + "node+0x9fd556", + "node+0x9f8d6c", + "node+0x9f8f68", + "V8::ExitFrame+0 in :0", + "handleWriteReq+0 in node:internal/stream_base_commons:0", + "writeGeneric+0 in node:internal/stream_base_commons:0", + "Socket._writeGeneric+0 in node:net:0", + "Socket._write+0 in node:net:0", + "writeOrBuffer+0 in node:internal/streams/writable:0", + "_write+0 in node:internal/streams/writable:0", + "Writable.write+0 in node:internal/streams/writable:0", + "value+0 in node:internal/console/constructor:0", + "log+0 in node:internal/console/constructor:0", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xc93180", + "node+0xc940ba", + "node+0xb2af62", + "node+0xa2f3a4", + "V8::ExitFrame+0 in :0", + "bar+1 in /home/tteras/work/elastic/node/hello2.js:2", + "foo+1 in /home/tteras/work/elastic/node/hello2.js:7", + "doit+2 in /home/tteras/work/elastic/node/hello2.js:12", + "+18 in /home/tteras/work/elastic/node/hello2.js:19", + "Module._compile+46 in node:internal/modules/cjs/loader:1376", + "Module._extensions..js+46 in node:internal/modules/cjs/loader:1435", + "Module.load+13 in node:internal/modules/cjs/loader:1207", + "Module._load+73 in node:internal/modules/cjs/loader:1023", + "executeUserEntryPoint+8 in node:internal/modules/run_main:135", + "+27 in node:internal/main/run_main_module:28", + "V8::InternalFrame+0 in :0", + "V8::EntryFrame+0 in :0", + "node+0xc93180", + "node+0xc940ba", + "node+0xb2af62", + "node+0x8a84e8", + "node+0x95fe29", + "node+0x87c455", + "node+0x7d49ea", + "node+0x91f890", + "node+0x9202fb", + "node+0x88126e", + "ld-musl-x86_64.so.1+0x1c6d0", + "node+0x7a12dd" + ] + }, + { + "lwp": 18948, + "frames": [ + "" + ] + }, + { + "lwp": 18949, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x20285", + "libuv.so.1.0.0+0x224f3", + "libuv.so.1.0.0+0xf442", + "node+0x953310", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18950, + "frames": [ + "" + ] + }, + { + "lwp": 18951, + "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", + "libuv.so.1.0.0+0x1d749", + "node+0x94dd33", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18952, + "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", + "libuv.so.1.0.0+0x1d749", + "node+0x94dd33", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18953, + "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", + "libuv.so.1.0.0+0x1d749", + "node+0x94dd33", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18954, + "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", + "libuv.so.1.0.0+0x1d749", + "node+0x94dd33", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18955, + "frames": [ + "" + ] + }, + { + "lwp": 18956, + "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+0x5e389", + "libuv.so.1.0.0+0x1d5c3", + "node+0xa1d1d2", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + } + ], + "modules": [ + { + "ref": "a93c22f55a8d82467d9973d40d225a241203b23bf60c249919c706bfbd511818", + "local-path": "/usr/bin/node" + }, + { + "ref": "9e9263659e630c6a6c8cbcc6d0caa726ecbf7c125e791089f9014543afaf2323", + "local-path": "/usr/share/icu/73.2/icudt73l.dat" + }, + { + "ref": "496613c0201dcb5f15a4e0b778a3928d8a5286713392d5b78a21b7845ebc3589", + "local-path": "/usr/lib/libstdc++.so.6.0.32" + }, + { + "ref": "f6fcac31afe4fd648a630dd11f52feb9bc7d93a372f799f5e2dd6d77a4493ffa", + "local-path": "/usr/lib/libicuuc.so.73.2" + }, + { + "ref": "c298df913d5a0ed6845313a7ee73c8984e9eb97058c63846362924dfbf471b0a", + "local-path": "/usr/lib/libicui18n.so.73.2" + }, + { + "ref": "6729c64d0903df39f6c457bda34f57c3eef67047e846513af23471dca6896905", + "local-path": "/lib/libcrypto.so.3" + }, + { + "ref": "26db3154da8971320275367f65a673c9d574ecb0eacbf43b5e8b6a216a076790", + "local-path": "/usr/lib/libicudata.so.73.2" + }, + { + "ref": "69daeca81eb911328eb4a1731c7a5ca1ab5cbe12507d6cbbd2f2d5ea373363e5", + "local-path": "/usr/lib/libbrotlicommon.so.1.1.0" + }, + { + "ref": "a3a07607d41a4475162c5d53834944b6ec1dfbda4fcd41eb7d9d6740f328deb1", + "local-path": "/usr/lib/libgcc_s.so.1" + }, + { + "ref": "26d4781c4ec3a72bf6a6ec4a679a4267d3113fbfd2d3d40f1518f8dca628948e", + "local-path": "/lib/libssl.so.3" + }, + { + "ref": "d5239d27f3ca8193e067e8c89e7bfe3473cb2c1403aa000836cef685bcc0d19c", + "local-path": "/usr/lib/libnghttp2.so.14.25.0" + }, + { + "ref": "2c934fa476809d24c9a5b1b7b96cd6f98abb673e7827ffe0a0041e85d592f47f", + "local-path": "/usr/lib/libcares.so.2.7.1" + }, + { + "ref": "2fbc85dc18f92dae2bceca7eaba0c75589b29254bdffe64b8e9074ee727d5814", + "local-path": "/usr/lib/libbrotlienc.so.1.1.0" + }, + { + "ref": "6d65dbbc78ff68b69d459a245b04e841db0360eec3cd91b05949d7483bc06078", + "local-path": "/usr/lib/libbrotlidec.so.1.1.0" + }, + { + "ref": "312ef720edfec8fbe431952edafede1cac81030d20044706d4402a934bd7ca6f", + "local-path": "/usr/lib/libuv.so.1.0.0" + }, + { + "ref": "75fb8b12584466bc4965f2c6c8f37e948a57175d82636af1dc4e90f961c9977f", + "local-path": "/lib/libz.so.1.3" + }, + { + "ref": "b4c5ae19cd527ab6f922f82c99423c06929433b5bf3add6e02bfa9b0d6565b39", + "local-path": "/usr/lib/libada.so.2.6.0" + }, + { + "ref": "494982426b423f09a034681dcbaca31545f7bec018053198932d54a7160987c5", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "f40f0ee1aa34f077b676a3b6efd477adbe37090673e31ac90dcde115e8305405", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + } + ] +} diff --git a/utils/coredump/testdata/amd64/openssl.14327.json b/utils/coredump/testdata/amd64/openssl.14327.json new file mode 100644 index 00000000..14f6b5bf --- /dev/null +++ b/utils/coredump/testdata/amd64/openssl.14327.json @@ -0,0 +1,23 @@ +{ + "coredump-ref": "019365b70eb446b26375019b34c87bc0fe2cbd2f8b50b4f8de38f12b6b4670cf", + "threads": [ + { + "lwp": 14327, + "frames": [ + "libcrypto.so.1.1+0x19759b", + "libcrypto.so.1.1+0x19780a", + "libcrypto.so.1.1+0x1351e7", + "libcrypto.so.1.1+0x9fe54", + "libcrypto.so.1.1+0x9f1a5", + "libcrypto.so.1.1+0x9f588", + "openssl+0x3b39a", + "openssl+0x3c0b7", + "openssl+0x44c54", + "openssl+0x323f4", + "ld-musl-x86_64.so.1+0x1ca02", + "openssl+0x325b9" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/perl528.14.json b/utils/coredump/testdata/amd64/perl528.14.json new file mode 100644 index 00000000..aea5b87d --- /dev/null +++ b/utils/coredump/testdata/amd64/perl528.14.json @@ -0,0 +1,25 @@ +{ + "coredump-ref": "462c77ccad87047ed1c2dceed36af11abaa155823f03a65622a0e8a3c416c101", + "threads": [ + { + "lwp": 14, + "frames": [ + "libpthread-2.28.so+0x11471", + "perl+0x173cd4", + "perl+0x1766f9", + "perl+0x175496", + "perl+0x17601c", + "perl+0x14b7cd", + "perl+0xe602b", + "HelloWorld::print+0 in hi.pl:12", + "+0 in hi.pl:19", + "perl+0xe2c25", + "perl+0x5e116", + "perl+0x34401", + "libc-2.28.so+0x2409a", + "perl+0x34449" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/perl528.151.json b/utils/coredump/testdata/amd64/perl528.151.json new file mode 100644 index 00000000..0e4c30e2 --- /dev/null +++ b/utils/coredump/testdata/amd64/perl528.151.json @@ -0,0 +1,32 @@ +{ + "coredump-ref": "4f94861ca06fa1f2101387824db3bc2fc5c8af508f21e6a055d15a72fe338764", + "threads": [ + { + "lwp": 151, + "frames": [ + "libc-2.28.so+0x15c751", + "perl+0xfa5b8", + "perl+0xe1987", + "perl+0x11cc2c", + "HTML::Lint::Parser::_element_push+0 in /usr/share/perl5/HTML/Lint/Parser.pm:337", + "HTML::Lint::Parser::_start+0 in /usr/share/perl5/HTML/Lint/Parser.pm:142", + "perl+0xe2c25", + "perl+0x55f61", + "Parser.so+0x6458", + "Parser.so+0x78a1", + "Parser.so+0x89c7", + "Parser.so+0x8e46", + "Parser.so+0x958a", + "perl+0xec900", + "HTML::Lint::parse+0 in /usr/share/perl5/HTML/Lint.pm:135", + "+0 in /usr/bin/weblint:67", + "perl+0xe2c25", + "perl+0x5e116", + "perl+0x34401", + "libc-2.28.so+0x2409a", + "perl+0x34449" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/perl528.206.json b/utils/coredump/testdata/amd64/perl528.206.json new file mode 100644 index 00000000..b88be400 --- /dev/null +++ b/utils/coredump/testdata/amd64/perl528.206.json @@ -0,0 +1,31 @@ +{ + "coredump-ref": "3f5d5c988bf17f6ce616430a7a815bbc67bbb1bb5b98bd03e5d26988f49ae2a7", + "threads": [ + { + "lwp": 206, + "frames": [ + "libc-2.28.so+0xc66f4", + "libc-2.28.so+0xc6629", + "perl+0x141b17", + "main::fib+0 in a.pl:21", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "+0 in a.pl:32", + "perl+0xe2c25", + "perl+0x5e116", + "perl+0x34401", + "libc-2.28.so+0x2409a", + "perl+0x34449" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/perl528bking.13.json b/utils/coredump/testdata/amd64/perl528bking.13.json new file mode 100644 index 00000000..20a93361 --- /dev/null +++ b/utils/coredump/testdata/amd64/perl528bking.13.json @@ -0,0 +1,25 @@ +{ + "coredump-ref": "5fb536eb8fdcbd55af9a9a256a876e60b0801ddb1f75614570b6b77eec18dcf2", + "threads": [ + { + "lwp": 13, + "frames": [ + "libpthread-2.17.so+0xe6e0", + "libperl.so+0x1688b6", + "libperl.so+0x168956", + "libperl.so+0x1651ed", + "libperl.so+0x165d5b", + "libperl.so+0x13e63a", + "libperl.so+0xe3cd7", + "HelloWorld::print+0 in hi.pl:12", + "+0 in hi.pl:19", + "libperl.so+0xe0af2", + "libperl.so+0x6913e", + "perl+0x400d1a", + "libc-2.17.so+0x22554", + "perl+0x400d52" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/perl534.220234.json b/utils/coredump/testdata/amd64/perl534.220234.json new file mode 100644 index 00000000..eee68556 --- /dev/null +++ b/utils/coredump/testdata/amd64/perl534.220234.json @@ -0,0 +1,21 @@ +{ + "coredump-ref": "d7fa9bf0effe15fd628f1cfa50a2e2d22c574f2c3e8e794f248b34c6b0e6fc66", + "threads": [ + { + "lwp": 220234, + "frames": [ + "libperl.so.5.34.0+0x14ea24", + "libperl.so.5.34.0+0x14eb69", + "HelloWorld::print+0 in hi.pl:11", + "+0 in hi.pl:19", + "libperl.so.5.34.0+0x10071f", + "libperl.so.5.34.0+0x7eafb", + "perl+0x1349", + "libc.so.6+0x2d55f", + "libc.so.6+0x2d60b", + "perl+0x1384" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/perl536-a.json b/utils/coredump/testdata/amd64/perl536-a.json new file mode 100644 index 00000000..7b26cf68 --- /dev/null +++ b/utils/coredump/testdata/amd64/perl536-a.json @@ -0,0 +1,33 @@ +{ + "coredump-ref": "2133172239f9a1934df5159c20f137f8fb128b497662fb6c419556a4be5cc9b6", + "threads": [ + { + "lwp": 40852, + "frames": [ + "libc.so.6+0xd2057", + "libc.so.6+0xd6846", + "libc.so.6+0xd677d", + "libperl.so.5.36.0+0x173c8b", + "main::fib+0 in a.pl:21", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "main::fib+0 in a.pl:24", + "+0 in a.pl:32", + "libperl.so.5.36.0+0x11084f", + "libperl.so.5.36.0+0x80fa0", + "perl+0x1349", + "libc.so.6+0x2750f", + "libc.so.6+0x275c8", + "perl+0x1384" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/perl536-helloWorld.json b/utils/coredump/testdata/amd64/perl536-helloWorld.json new file mode 100644 index 00000000..cd105160 --- /dev/null +++ b/utils/coredump/testdata/amd64/perl536-helloWorld.json @@ -0,0 +1,26 @@ +{ + "coredump-ref": "507e21408db838770bd5bd36e3a068eb22d6a6f5bc727aeedf13f6c6a29cf002", + "threads": [ + { + "lwp": 26718, + "frames": [ + "libc.so.6+0xfb0c4", + "libperl.so.5.36.0+0x1a15fc", + "libperl.so.5.36.0+0x1a0880", + "libperl.so.5.36.0+0x19781f", + "libperl.so.5.36.0+0x197fc3", + "libperl.so.5.36.0+0x17d7cd", + "libperl.so.5.36.0+0x113bf3", + "HelloWorld::print+0 in hi.pl:12", + "+0 in hi.pl:19", + "libperl.so.5.36.0+0x11084f", + "libperl.so.5.36.0+0x80fa0", + "perl+0x1349", + "libc.so.6+0x2750f", + "libc.so.6+0x275c8", + "perl+0x1384" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/php-8.2.10-prime.json b/utils/coredump/testdata/amd64/php-8.2.10-prime.json new file mode 100644 index 00000000..23cd871b --- /dev/null +++ b/utils/coredump/testdata/amd64/php-8.2.10-prime.json @@ -0,0 +1,196 @@ +{ + "coredump-ref": "bc5ec005d3df5ce89dee982679b348e01269ac6d0763a8cdf0cd08dcdf7dc69d", + "threads": [ + { + "lwp": 24450, + "frames": [ + "php8.2+0x322dfd", + "is_prime+13 in /pwd/utils/coredump/testsources/php/prime.php:16", + "+33 in /pwd/utils/coredump/testsources/php/prime.php:34", + "php8.2+0x361a38", + "php8.2+0x36b434", + "php8.2+0x2f872f", + "php8.2+0x292619", + "php8.2+0x3e0f66", + "php8.2+0x1281af", + "libc.so.6+0x276c9", + "libc.so.6+0x27784", + "php8.2+0x129320" + ] + } + ], + "modules": [ + { + "ref": "23041999758a28e7ff5ecdac6644830fdb6d44403cd132c2fbd74022e42cf38f", + "local-path": "/usr/bin/php8.2" + }, + { + "ref": "11277237720b2cfd43bcd62e936b325d19c8960488b34f89b00c49e355b7446a", + "local-path": "/usr/lib/php/20220829/tokenizer.so" + }, + { + "ref": "3547d47ee77fc02a04f1e89a2322ff3255cc59f5ada11af5b7732b282eaa05cd", + "local-path": "/usr/lib/php/20220829/sysvshm.so" + }, + { + "ref": "4d89cfd20437cf72e045997a64ebdf98bbe53810dd0df0a4f4a0bb0c6a4e9546", + "local-path": "/usr/lib/php/20220829/sysvsem.so" + }, + { + "ref": "6a64e15329cd3808478a9ce130b1211a2c90ac57ca4dccd9c68d062d0bd4990c", + "local-path": "/usr/lib/php/20220829/sysvmsg.so" + }, + { + "ref": "161c62cd9b707218a9eafbf2c66752b5c8aa5e7a5bab464f45e21a467f013dc4", + "local-path": "/usr/lib/php/20220829/sockets.so" + }, + { + "ref": "ae7e47f3da21fba71d4a4805d22009770f6c8471533512c533f808d32f0e2b11", + "local-path": "/usr/lib/php/20220829/shmop.so" + }, + { + "ref": "3a5843da5431009888a5f16a66f4511088cac86a6142b4c169e1a374f6c6c47a", + "local-path": "/usr/lib/x86_64-linux-gnu/libmd.so.0.1.0" + }, + { + "ref": "b6e4d7092d87d3d57b01cf356b0c2c79ab2966c7397fedcf2cec148a2a85cb1c", + "local-path": "/usr/lib/x86_64-linux-gnu/libbsd.so.0.11.7" + }, + { + "ref": "90f662a7ceaf457bed484b2a04743b48a62604a46968cc3ac943ba6b0da7d33d", + "local-path": "/usr/lib/x86_64-linux-gnu/libtinfo.so.6.4" + }, + { + "ref": "b05093e7a544698e5a02f7e28fb53f745a920fa32f82296f3c39d925277904c4", + "local-path": "/usr/lib/x86_64-linux-gnu/libedit.so.2.0.72" + }, + { + "ref": "74db0c208133c47714503e52fa8f92ca84ba0510d07ec6656fc03ff05fd12d74", + "local-path": "/usr/lib/php/20220829/readline.so" + }, + { + "ref": "a77d27fb629b4191a8f0f5a44014505c5c4c223a637bffa3e5663ef2a530e889", + "local-path": "/usr/lib/php/20220829/posix.so" + }, + { + "ref": "7073c5071b353f37d775c94b6bd9d0ea633204bce7f13702b1a9c3e047baec8b", + "local-path": "/usr/lib/php/20220829/phar.so" + }, + { + "ref": "7f5e498195438bc1398ac4512601d7725730ecd1d988248e5952b63c9d4e1ddd", + "local-path": "/usr/lib/php/20220829/iconv.so" + }, + { + "ref": "4dd088f947f1b639be512a75faf258641c1de3741ffd6bb3d658552bc562f639", + "local-path": "/usr/lib/php/20220829/gettext.so" + }, + { + "ref": "9606fa59035e0f2feb6d050f11b28c42303474c1ecc4081da7bbd88f377defce", + "local-path": "/usr/lib/php/20220829/ftp.so" + }, + { + "ref": "f295b2954cf876d2ec347e53a8867b0cbd5bb6f9a3646cd4be5117f415e07479", + "local-path": "/usr/lib/php/20220829/fileinfo.so" + }, + { + "ref": "983e72b7e964f3db43fe8a3dc8b338e731fade6ca38df6867aebb58186aaeb68", + "local-path": "/usr/lib/x86_64-linux-gnu/libffi.so.8.1.2" + }, + { + "ref": "d19da86eaf39766f5ea538cc2ebf356d8c67e8c34cd46a775c200f625468c785", + "local-path": "/usr/lib/php/20220829/ffi.so" + }, + { + "ref": "42e2383fd129a4144ad1a2c30915ed78383a1b6dc3ff2471bf49e179480cff11", + "local-path": "/usr/lib/php/20220829/exif.so" + }, + { + "ref": "5fbf5675a12c8aee0a3718be2bccba65c03e0e997581940c1aec44b6eeb4b964", + "local-path": "/usr/lib/php/20220829/ctype.so" + }, + { + "ref": "0139c264736ca7cd9990cba1a6cfb16f2a052b804f6d050bbf50261ed7781318", + "local-path": "/usr/lib/php/20220829/pdo.so" + }, + { + "ref": "665bd2a71822cc6bdcaed9a5f489f2cca0b39a17bb0ff63c9133469777c5efbd", + "local-path": "/usr/lib/php/20220829/calendar.so" + }, + { + "ref": "24d20d1355dcee9b18e1a6928975646cb4116d0a4fbb2d958a40cb3acdfb7c90", + "local-path": "/usr/lib/php/20220829/opcache.so" + }, + { + "ref": "52c227df9d53248238602c1ddaccd2c8ddc4cc6a61aa45d7c425af590b8806a5", + "local-path": "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "4af23bb40c8f2e80a26c95369b442986213c50a7308d8d73b85c4911dde0a358", + "local-path": "/usr/lib/locale/C.utf8/LC_CTYPE" + }, + { + "ref": "180f0bf1a4cf65471a56c08bbe70a90774626e9ed5dd7352445973da5fbd465c", + "local-path": "/usr/lib/x86_64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "c90d53b162182e1e98fc971ea3aa485f831ecd31ad228e93040c8972c30d4e7a", + "local-path": "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.32" + }, + { + "ref": "b9574df8f21b38490cbd5829c0273f08d89252ef7edf0440f4cc89137f57db6d", + "local-path": "/usr/lib/x86_64-linux-gnu/libicudata.so.72.1" + }, + { + "ref": "fbd0ca396e5f2817f5387f024c30069758874e97a1195ab489ca70cf9789ac37", + "local-path": "/usr/lib/x86_64-linux-gnu/libpthread.so.0" + }, + { + "ref": "8f843e5b2b42b38629ecd85c82a21f64b6ff253034f0ba2ad9b30545a2c4714a", + "local-path": "/usr/lib/x86_64-linux-gnu/liblzma.so.5.4.4" + }, + { + "ref": "1b87b33099db47a35b50f918ae86800247acc9f3f6d2fbe449aea3ce83ab9cf1", + "local-path": "/usr/lib/x86_64-linux-gnu/libicuuc.so.72.1" + }, + { + "ref": "124f193df4593043ed0c67f986cdd8174cd05a5410329db38ce4fc5d0a2171a0", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "ae3ab30156dcc744bc91be18e585b53d17d8e0aff322c0205c1ccd9c5071bd3b", + "local-path": "/usr/lib/x86_64-linux-gnu/libargon2.so.1" + }, + { + "ref": "77d780ac0abcbb02175e98a74c246af5fe4b3fe2cd6262b907c0d13c8725d732", + "local-path": "/usr/lib/x86_64-linux-gnu/libsodium.so.23.3.0" + }, + { + "ref": "2a6e6bba32d40d7d68f5226c7f4085c3aaf527d62dd852b6808cb21dfeedbc97", + "local-path": "/usr/lib/x86_64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "8b0272ad55cfb4a57ec42aeecf1f5859c4444c7abf2d2de5ca18bcf42809e0db", + "local-path": "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0.11.2" + }, + { + "ref": "b1dced69346b2ff8a99c10882630a43656807865bfc59595c4631e53d84f8ac4", + "local-path": "/usr/lib/x86_64-linux-gnu/libcrypto.so.3" + }, + { + "ref": "d9aec794cf923fe6a291aa016393df510195b642de11526ccd3a63ac14efa2fb", + "local-path": "/usr/lib/x86_64-linux-gnu/libssl.so.3" + }, + { + "ref": "207c384714d75fa7af44ff7e5a01337ed2fceba7e1c1b92fb22fb17b977d430b", + "local-path": "/usr/lib/x86_64-linux-gnu/libxml2.so.2.9.14" + }, + { + "ref": "6d980378562a20082a35a45b571a9a2ea537f447ddeb7534d5110c6fbed8aa91", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "d560c04183ef2a5f3ece1d89e0d68b9d5ceb6088b9c8ac3abd3a64605d4d70dd", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + } + ] +} diff --git a/utils/coredump/testdata/amd64/php74.7893.json b/utils/coredump/testdata/amd64/php74.7893.json new file mode 100644 index 00000000..fdef3c86 --- /dev/null +++ b/utils/coredump/testdata/amd64/php74.7893.json @@ -0,0 +1,22 @@ +{ + "coredump-ref": "9ad2317b116884d61014c03f86e01054ec95e84bafcf50f031c44ede8f6ba021", + "threads": [ + { + "lwp": 7893, + "frames": [ + "php7.4+0x2e9b52", + "is_prime+13 in /home/j/prime.php:16", + "+33 in /home/j/prime.php:34", + "php7.4+0x321863", + "php7.4+0x32a09a", + "php7.4+0x2a2b53", + "php7.4+0x2417ef", + "php7.4+0x32c147", + "php7.4+0x10d870", + "libc-2.31.so+0x26d09", + "php7.4+0x10d9e9" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/python310.stringbench.20086.json b/utils/coredump/testdata/amd64/python310.stringbench.20086.json new file mode 100644 index 00000000..f8367d6d --- /dev/null +++ b/utils/coredump/testdata/amd64/python310.stringbench.20086.json @@ -0,0 +1,20 @@ +{ + "coredump-ref": "94a9c5cd6295d08274ff0c4f513a0157b220675c252753e2bd7598f3d4017dde", + "threads": [ + { + "lwp": 20086, + "frames": [ + "libpython3.10.so.1.0+0x126a16", + "libpython3.10.so.1.0+0x190a07", + "libpython3.10.so.1.0+0x11e986", + "rpartition_test_slow_match_two_characters+2 in /home/flehner/src/github.com/python/cpython/Tools/stringbench/stringbench.py:478", + "inner+1 in :3", + "timeit+10 in /usr/lib64/python3.10/timeit.py:174", + "best+1 in /home/flehner/src/github.com/python/cpython/Tools/stringbench/stringbench.py:1399", + "main+1 in /home/flehner/src/github.com/python/cpython/Tools/stringbench/stringbench.py:1410", + "+4 in /home/flehner/src/github.com/python/cpython/Tools/stringbench/stringbench.py:5" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/python310.stringbench.2576.json b/utils/coredump/testdata/amd64/python310.stringbench.2576.json new file mode 100644 index 00000000..80400cca --- /dev/null +++ b/utils/coredump/testdata/amd64/python310.stringbench.2576.json @@ -0,0 +1,58 @@ +{ + "coredump-ref": "eb13ae9e124fc558e2f822d3b272a416bef09eb3112bbdf06aaccc75deba370d", + "threads": [ + { + "lwp": 2576, + "frames": [ + "python3.10+0x1939bf", + "python3.10+0x1f4f2a", + "python3.10+0x187eb7", + "python3.10+0x17ebc2", + "find_test_no_match_two_character_bis+2 in /home/admin/stringbench.py:175", + "inner+1 in :3", + "timeit+10 in /usr/lib/python3.10/timeit.py:174", + "best+1 in /home/admin/stringbench.py:1399", + "main+1 in /home/admin/stringbench.py:1410", + "+4 in /home/admin/stringbench.py:5" + ] + } + ], + "modules": [ + { + "ref": "1f3be39333e8732773cad83596b705b4b6337d709b7fd20724a31d298644399f", + "local-path": "/usr/lib/x86_64-linux-gnu/libc-2.33.so" + }, + { + "ref": "e3e86a9b6f212417608d45b3e0e73780bb2a022150628c03a799c2e3a94e98a4", + "local-path": "/usr/lib/x86_64-linux-gnu/libexpat.so.1.8.8" + }, + { + "ref": "614f68c5a280c4360e81d4e25a40855512cebb6d0c58853435606daf1f1a71c1", + "local-path": "/usr/lib/x86_64-linux-gnu/libutil-2.33.so" + }, + { + "ref": "c1b7e20130ef1adfa1480c18eafc1ba6c5e9eb56b251883aa85bedbfb1e50def", + "local-path": "/usr/lib/x86_64-linux-gnu/libdl-2.33.so" + }, + { + "ref": "e06cf484fb812a803e6b7e5379f0abd460a834efe6edc3ea6efa8f931eddbcb4", + "local-path": "/usr/lib/x86_64-linux-gnu/libpthread-2.33.so" + }, + { + "ref": "83d60495d0775515ead6c551cd74c6fd4dcf7d2a087bbf935765c0ba040faa1f", + "local-path": "/usr/lib/x86_64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "ba0c0bd8cf45c7100fb6705d2e2abf711042b69afbe59bfc784202e7ebd7157a", + "local-path": "/usr/lib/x86_64-linux-gnu/libm-2.33.so" + }, + { + "ref": "cd1aa26ea490f4d7692d8f966e8cf7b3c41a1338914fa44ea4b80cf3a2a01b8c", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-2.33.so" + }, + { + "ref": "abc9170dfb10b8a926d2376de94aa9a0ffd7b0ea4febf80606b4bba6c5ffa386", + "local-path": "/usr/bin/python3.10" + } + ] +} diff --git a/utils/coredump/testdata/amd64/python311-expat.json b/utils/coredump/testdata/amd64/python311-expat.json new file mode 100644 index 00000000..7d99a604 --- /dev/null +++ b/utils/coredump/testdata/amd64/python311-expat.json @@ -0,0 +1,68 @@ +{ + "coredump-ref": "5b13b3e4ef3ed1f7f5b62113200514b45285c445ab6cebfea7dd038053472202", + "threads": [ + { + "lwp": 25548, + "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+0x1bd1c9", + "pyexpat.cpython-311-x86_64-linux-musl.so+0x6f2e", + "libexpat.so.1.8.10+0xb51c", + "libexpat.so.1.8.10+0xbb37", + "libexpat.so.1.8.10+0xc3b9", + "libexpat.so.1.8.10+0xd833", + "libexpat.so.1.8.10+0x7898", + "pyexpat.cpython-311-x86_64-linux-musl.so+0x6bee", + "libpython3.11.so.1.0+0x21c1da", + "libpython3.11.so.1.0+0x1cc093", + "main+7 in /home/tteras/work/elastic/python/expat.py:23", + "+27 in /home/tteras/work/elastic/python/expat.py:28", + "libpython3.11.so.1.0+0x1bf4b4", + "libpython3.11.so.1.0+0x1bd1c9", + "libpython3.11.so.1.0+0x245657", + "libpython3.11.so.1.0+0x265472", + "libpython3.11.so.1.0+0x261ac9", + "libpython3.11.so.1.0+0x277251", + "libpython3.11.so.1.0+0x276774", + "libpython3.11.so.1.0+0x276493", + "libpython3.11.so.1.0+0x270c06", + "libpython3.11.so.1.0+0x235b06", + "ld-musl-x86_64.so.1+0x1c9c9", + "python3.11+0x1075", + "" + ] + } + ], + "modules": [ + { + "ref": "ab8be098465c68bc1f218be3784058347c752bd1f1b4fd3ee465d4ac10e19be6", + "local-path": "/usr/lib/libexpat.so.1.8.10" + }, + { + "ref": "df9b50d7d109e06c63878f8b1b01edaa75f1876312b0b81aa0e3217d0e5cf0c0", + "local-path": "/usr/lib/python3.11/lib-dynload/pyexpat.cpython-311-x86_64-linux-musl.so" + }, + { + "ref": "b87ba1ea40af6305503e4377439e271672ca8e7a045095066421caafd87d248f", + "local-path": "/usr/lib/debug/usr/lib/python3.11/lib-dynload/pyexpat.cpython-311-x86_64-linux-musl.so.debug" + }, + { + "ref": "b14a0e943b0480bd6d590fa0b2b2734763b3e134625e84ab1c363bb2f77e0a2a", + "local-path": "/usr/lib/libpython3.11.so.1.0" + }, + { + "ref": "a0e80898190e34005a4d0598fa71e2e0b0ab2726a3cd73c3ad147770ca371173", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "c63ff448b1840dd2eb5ca1630eb4edc2e4104a6e944bea0b34539c882caae0d5", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "79244af6389e0530139f967cf82f7161116ae4b454d52363f1668744cbe2d40a", + "local-path": "/usr/bin/python3.11" + } + ] +} diff --git a/utils/coredump/testdata/amd64/python37.26320.json b/utils/coredump/testdata/amd64/python37.26320.json new file mode 100644 index 00000000..83be5219 --- /dev/null +++ b/utils/coredump/testdata/amd64/python37.26320.json @@ -0,0 +1,21 @@ +{ + "coredump-ref": "3740f861518da26014fc27f693555b7d9f2974d7f0e3c39e7eb15cca8b119b09", + "threads": [ + { + "lwp": 26320, + "frames": [ + "python3.7+0x532049", + "python3.7+0x5329a0", + "python3.7+0x4d9964", + "python3.7+0x5ccf60", + "python3.7+0x5d0228", + "python3.7+0x5d0548", + "python3.7+0x4c27c2", + "python3.7+0x54ab47", + "python3.7+0x5ccd0b", + "+2 in /tmp/systemtest_tmppy_1612432164662592633.py:3" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/ruby-2.7.8p225-loop.json b/utils/coredump/testdata/amd64/ruby-2.7.8p225-loop.json new file mode 100644 index 00000000..2e554082 --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby-2.7.8p225-loop.json @@ -0,0 +1,108 @@ +{ + "coredump-ref": "3d28ac93d85962f6a74ce270a09c486670177b5051b98442d9eea86acc728509", + "threads": [ + { + "lwp": 35308, + "frames": [ + "libruby.so.2.7.8+0x2106b3", + "libruby.so.2.7.8+0x22a09a", + "libruby.so.2.7.8+0x22cb42", + "libruby.so.2.7.8+0x18009f", + "libruby.so.2.7.8+0x180f84", + "libruby.so.2.7.8+0x216781", + "libruby.so.2.7.8+0x211485", + "libruby.so.2.7.8+0x2212d2", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.2.7.8+0x226a9a", + "libruby.so.2.7.8+0x232da4", + "libruby.so.2.7.8+0x180c1d", + "libruby.so.2.7.8+0x216781", + "libruby.so.2.7.8+0x211485", + "libruby.so.2.7.8+0x2212d2", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.2.7.8+0x226a9a", + "libruby.so.2.7.8+0x229901", + "libruby.so.2.7.8+0xae306", + "libruby.so.2.7.8+0xae519", + "libruby.so.2.7.8+0x216781", + "libruby.so.2.7.8+0x211485", + "libruby.so.2.7.8+0x2212d2", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.2.7.8+0x226a9a", + "libruby.so.2.7.8+0xabecf", + "libruby.so.2.7.8+0xb0297", + "ruby+0x110a", + "libc-2.31.so+0x23d09", + "ruby+0x1159" + ] + } + ], + "modules": [ + { + "ref": "af2ba550be5ff68f1fc659e540cd1336fd4af1e2767185e74e5e98f2a5daa255", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "49287d03ed0d1d981bfb7ecf8e1ff27011beb709590930e4b4ef44852a377559", + "local-path": "/usr/local/lib/ruby/2.7.0/x86_64-linux/monitor.so" + }, + { + "ref": "503a6a7f0a4c3e4601462c55c0df4b0376b4f49a7b1f42d6e58cdcd691dafc4d", + "local-path": "/usr/local/lib/ruby/2.7.0/x86_64-linux/enc/trans/transdb.so" + }, + { + "ref": "ee36eeebb99e31aec5b7439ed0b7b4c8a33b91c188458f7bb261dc0f732744f3", + "local-path": "/usr/local/lib/ruby/2.7.0/x86_64-linux/enc/encdb.so" + }, + { + "ref": "ad0110dd4067acf54418115178a976927cc31c894e50919270565a909e7f9aed", + "local-path": "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "5461e535328078fb72f25f7ca2bf36bc71c415bbf573ccdfa289a7b796ddfdd3", + "local-path": "/lib/x86_64-linux-gnu/libm-2.31.so" + }, + { + "ref": "cef2a912ebcad80814c5bf4cabb4a5fe2d8d680be98c9711796e0f773e5ccb06", + "local-path": "/lib/x86_64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "df5973c718b4ee540cdd4400e19e90182ab5f3bac2148c0c9f3bb92eb6af3121", + "local-path": "/lib/x86_64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "1ce1c61a3bb0eff2c58b3ff3a473972dc7d95d45fba8abaa0e5e993d923e80c2", + "local-path": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "a24ccd19ad1dc82d78d825553c4ad8332e25f879970fe602d230312d93b5ce23", + "local-path": "/lib/x86_64-linux-gnu/librt-2.31.so" + }, + { + "ref": "9807a603f7e37097a442126303e0705a85f38ad4926db8eaf5db4befad118e11", + "local-path": "/lib/x86_64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "062372f2053424d8bafd9378ebdcc29a08d7e1850342b3fd5a0e73462ed84901", + "local-path": "/lib/x86_64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "e09732683527119fd0a666c88c3dec725094c20815a207be020fe68402c8b8a2", + "local-path": "/lib/x86_64-linux-gnu/libc-2.31.so" + }, + { + "ref": "4119af6ddb298b3a9e4e5ae0a2668c2dd2e14834fc3d6061dc572d8528952541", + "local-path": "/usr/local/lib/libruby.so.2.7.8" + }, + { + "ref": "4325e436d7b281490848dfb580f16844adcf31afedf3972e12a660abe5c057c2", + "local-path": "/lib/x86_64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/amd64/ruby-3.0.4p208-loop.json b/utils/coredump/testdata/amd64/ruby-3.0.4p208-loop.json new file mode 100644 index 00000000..b1731c1e --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby-3.0.4p208-loop.json @@ -0,0 +1,105 @@ +{ + "coredump-ref": "f3e77bf8aa75a80aeefee56a541f3c20990aa2348c1592a7b7e5db0224ab5693", + "threads": [ + { + "lwp": 15971, + "frames": [ + "libruby.so.3.0.4+0x2b56b9", + "libruby.so.3.0.4+0x1db133", + "libruby.so.3.0.4+0x29b46b", + "libruby.so.3.0.4+0x2a640e", + "libruby.so.3.0.4+0x2a9372", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.0.4+0x2af0ad", + "libruby.so.3.0.4+0x2bbd7a", + "libruby.so.3.0.4+0x1daf5d", + "libruby.so.3.0.4+0x29b46b", + "libruby.so.3.0.4+0x2a640e", + "libruby.so.3.0.4+0x2a9372", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.0.4+0x2ae871", + "libruby.so.3.0.4+0x2b0021", + "libruby.so.3.0.4+0xd70da", + "libruby.so.3.0.4+0xd7349", + "libruby.so.3.0.4+0x29b46b", + "libruby.so.3.0.4+0x2a640e", + "libruby.so.3.0.4+0x2a9372", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.0.4+0x2ae871", + "libruby.so.3.0.4+0xd3dd5", + "libruby.so.3.0.4+0xd9aa5", + "ruby+0x110a", + "libc-2.31.so+0x23d09", + "ruby+0x1159" + ] + } + ], + "modules": [ + { + "ref": "398cf918d69138d84264df86de4aca05cb02945c042a4f35c9e1781d1ffe0b32", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "377c923c978a8f7443ad9790e98ec6616a8dc9e6e82cd6aab92b8dd8199fe55c", + "local-path": "/usr/local/lib/ruby/3.0.0/x86_64-linux/monitor.so" + }, + { + "ref": "75fc54a2910e4e4211cc573d33ac6fd1f7df6ceb065c8a79641fd0a53556feab", + "local-path": "/usr/local/lib/ruby/3.0.0/x86_64-linux/enc/trans/transdb.so" + }, + { + "ref": "fb07205ed83d03cf02877df7c5184d85f9db878ab05466f513f1a010e218f67d", + "local-path": "/usr/local/lib/ruby/3.0.0/x86_64-linux/enc/encdb.so" + }, + { + "ref": "ad0110dd4067acf54418115178a976927cc31c894e50919270565a909e7f9aed", + "local-path": "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "263e55f639b540a00fc87e7fdc10078c1cbe89e148ea440ad1a9cef1cbd53c6f", + "local-path": "/lib/x86_64-linux-gnu/libc-2.31.so" + }, + { + "ref": "5461e535328078fb72f25f7ca2bf36bc71c415bbf573ccdfa289a7b796ddfdd3", + "local-path": "/lib/x86_64-linux-gnu/libm-2.31.so" + }, + { + "ref": "cef2a912ebcad80814c5bf4cabb4a5fe2d8d680be98c9711796e0f773e5ccb06", + "local-path": "/lib/x86_64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "df5973c718b4ee540cdd4400e19e90182ab5f3bac2148c0c9f3bb92eb6af3121", + "local-path": "/lib/x86_64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "1ce1c61a3bb0eff2c58b3ff3a473972dc7d95d45fba8abaa0e5e993d923e80c2", + "local-path": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "a24ccd19ad1dc82d78d825553c4ad8332e25f879970fe602d230312d93b5ce23", + "local-path": "/lib/x86_64-linux-gnu/librt-2.31.so" + }, + { + "ref": "9807a603f7e37097a442126303e0705a85f38ad4926db8eaf5db4befad118e11", + "local-path": "/lib/x86_64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "062372f2053424d8bafd9378ebdcc29a08d7e1850342b3fd5a0e73462ed84901", + "local-path": "/lib/x86_64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "7d2ea8bd8be663c506b1c57150963957554e6078c2945daa8c69b5cea7c64fd1", + "local-path": "/usr/local/lib/libruby.so.3.0.4" + }, + { + "ref": "4325e436d7b281490848dfb580f16844adcf31afedf3972e12a660abe5c057c2", + "local-path": "/lib/x86_64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/amd64/ruby-3.0.6p216-loop.json b/utils/coredump/testdata/amd64/ruby-3.0.6p216-loop.json new file mode 100644 index 00000000..ab03ebab --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby-3.0.6p216-loop.json @@ -0,0 +1,107 @@ +{ + "coredump-ref": "75df37e961a1b8a1263b1eeef8462addf677066b79841f497af941198456836d", + "threads": [ + { + "lwp": 17426, + "frames": [ + "libruby.so.3.0.6+0x2942b0", + "libruby.so.3.0.6+0x2b1ce4", + "libruby.so.3.0.6+0x2b566b", + "libruby.so.3.0.6+0x1db3e3", + "libruby.so.3.0.6+0x29b7db", + "libruby.so.3.0.6+0x2a677e", + "libruby.so.3.0.6+0x2a96d2", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.0.6+0x2af40d", + "libruby.so.3.0.6+0x2bc0ea", + "libruby.so.3.0.6+0x1db20d", + "libruby.so.3.0.6+0x29b7db", + "libruby.so.3.0.6+0x2a677e", + "libruby.so.3.0.6+0x2a96d2", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.0.6+0x2aebd1", + "libruby.so.3.0.6+0x2b0381", + "libruby.so.3.0.6+0xd710a", + "libruby.so.3.0.6+0xd7379", + "libruby.so.3.0.6+0x29b7db", + "libruby.so.3.0.6+0x2a677e", + "libruby.so.3.0.6+0x2a96d2", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.0.6+0x2aebd1", + "libruby.so.3.0.6+0xd3e05", + "libruby.so.3.0.6+0xd9ad5", + "ruby+0x110a", + "libc-2.31.so+0x23d09", + "ruby+0x1159" + ] + } + ], + "modules": [ + { + "ref": "21952f1eb03236e6d195f22d524e1dfbf6fe636599f564c82c6cd2657db249ed", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "a2e3c577ab2e4df502b17993c32a89a31f9a06c228d0866336c8bd7934cdf96d", + "local-path": "/usr/local/lib/ruby/3.0.0/x86_64-linux/monitor.so" + }, + { + "ref": "75fc54a2910e4e4211cc573d33ac6fd1f7df6ceb065c8a79641fd0a53556feab", + "local-path": "/usr/local/lib/ruby/3.0.0/x86_64-linux/enc/trans/transdb.so" + }, + { + "ref": "1c262e4887a4150e0fb7f93465c842d2a73de56495471a15d8b3eede4f5a2cdd", + "local-path": "/usr/local/lib/ruby/3.0.0/x86_64-linux/enc/encdb.so" + }, + { + "ref": "ad0110dd4067acf54418115178a976927cc31c894e50919270565a909e7f9aed", + "local-path": "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "e09732683527119fd0a666c88c3dec725094c20815a207be020fe68402c8b8a2", + "local-path": "/lib/x86_64-linux-gnu/libc-2.31.so" + }, + { + "ref": "5461e535328078fb72f25f7ca2bf36bc71c415bbf573ccdfa289a7b796ddfdd3", + "local-path": "/lib/x86_64-linux-gnu/libm-2.31.so" + }, + { + "ref": "cef2a912ebcad80814c5bf4cabb4a5fe2d8d680be98c9711796e0f773e5ccb06", + "local-path": "/lib/x86_64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "df5973c718b4ee540cdd4400e19e90182ab5f3bac2148c0c9f3bb92eb6af3121", + "local-path": "/lib/x86_64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "1ce1c61a3bb0eff2c58b3ff3a473972dc7d95d45fba8abaa0e5e993d923e80c2", + "local-path": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "a24ccd19ad1dc82d78d825553c4ad8332e25f879970fe602d230312d93b5ce23", + "local-path": "/lib/x86_64-linux-gnu/librt-2.31.so" + }, + { + "ref": "9807a603f7e37097a442126303e0705a85f38ad4926db8eaf5db4befad118e11", + "local-path": "/lib/x86_64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "062372f2053424d8bafd9378ebdcc29a08d7e1850342b3fd5a0e73462ed84901", + "local-path": "/lib/x86_64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "7cd97e2d4c26c6b70d1c93fee352621e661d85d56d1264213034295f4c2739f3", + "local-path": "/usr/local/lib/libruby.so.3.0.6" + }, + { + "ref": "4325e436d7b281490848dfb580f16844adcf31afedf3972e12a660abe5c057c2", + "local-path": "/lib/x86_64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/amd64/ruby-3.1.0p0-loop.json b/utils/coredump/testdata/amd64/ruby-3.1.0p0-loop.json new file mode 100644 index 00000000..db59b911 --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby-3.1.0p0-loop.json @@ -0,0 +1,105 @@ +{ + "coredump-ref": "161ff49cb6e8976d57914d1aa8d165d6a7f3c70340b378dc6b983811ab78418c", + "threads": [ + { + "lwp": 18920, + "frames": [ + "libruby.so.3.1.0+0x2cb036", + "libruby.so.3.1.0+0x1eb413", + "libruby.so.3.1.0+0x2ad99b", + "libruby.so.3.1.0+0x2b30c8", + "libruby.so.3.1.0+0x2bca6e", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.1.0+0x2c2745", + "libruby.so.3.1.0+0x2c6ad2", + "libruby.so.3.1.0+0x1eb27d", + "libruby.so.3.1.0+0x2ad99b", + "libruby.so.3.1.0+0x2b30c8", + "libruby.so.3.1.0+0x2bca6e", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.1.0+0x2c1e6e", + "libruby.so.3.1.0+0x2c32aa", + "libruby.so.3.1.0+0xddbda", + "libruby.so.3.1.0+0xdde49", + "libruby.so.3.1.0+0x2ad99b", + "libruby.so.3.1.0+0x2b30c8", + "libruby.so.3.1.0+0x2bca6e", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.1.0+0x2c1e6e", + "libruby.so.3.1.0+0xda277", + "libruby.so.3.1.0+0xe0435", + "ruby+0x110a", + "libc-2.31.so+0x26d09", + "ruby+0x1159" + ] + } + ], + "modules": [ + { + "ref": "5d353e2907b94dffd7c2a3215f6dfc880b7c935765a388b568b77d14dfc81fba", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "81e984d5aecfbceaf21c50f9b273a52947e63018f2eb581a8e8a31d6bcbcaefb", + "local-path": "/usr/local/lib/ruby/3.1.0/x86_64-linux/monitor.so" + }, + { + "ref": "de74d485a79599033d56cff7c8c66ab354b5f3d1ed812b6e53512b3cb9162794", + "local-path": "/usr/local/lib/ruby/3.1.0/x86_64-linux/enc/trans/transdb.so" + }, + { + "ref": "1327c2501557326a20ca5f5aed637b486abffcdef71c75b597864c1028aa880a", + "local-path": "/usr/local/lib/ruby/3.1.0/x86_64-linux/enc/encdb.so" + }, + { + "ref": "ad0110dd4067acf54418115178a976927cc31c894e50919270565a909e7f9aed", + "local-path": "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "17abce7433ccac97a7a5e9c1d286b7337c30d01f25e0d837cfe78de9be61e768", + "local-path": "/lib/x86_64-linux-gnu/libc-2.31.so" + }, + { + "ref": "8ee97a8efb90621d80585e43483efa292fa9e719983976404f6a9834189454d0", + "local-path": "/lib/x86_64-linux-gnu/libm-2.31.so" + }, + { + "ref": "cef2a912ebcad80814c5bf4cabb4a5fe2d8d680be98c9711796e0f773e5ccb06", + "local-path": "/lib/x86_64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "f88decec603a9d58fb512aff57a13c94bdf209f20feff2e63736f1060aad72b6", + "local-path": "/lib/x86_64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "1ce1c61a3bb0eff2c58b3ff3a473972dc7d95d45fba8abaa0e5e993d923e80c2", + "local-path": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "ea515fd5cd3166e653411f7e3b2750ed2450689b741c3047b37d57a6d557822c", + "local-path": "/lib/x86_64-linux-gnu/librt-2.31.so" + }, + { + "ref": "5105e71a3ac4ba330edf02190367a2de47c397beaa15bf9c86eccd699428985f", + "local-path": "/lib/x86_64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "9a5ff6ffc3dd61749a7d01aa7917a7a530d7be2f56e2fa32f9a41ff3fb18b94c", + "local-path": "/lib/x86_64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "9d26d921a11e70772466264517c78dc2e488c07b454a83e6a1e069531878ed20", + "local-path": "/usr/local/lib/libruby.so.3.1.0" + }, + { + "ref": "0a96b6c36729ebdc85b18e2bf036b1a1b4bd96e3c593c31b5154c3a1d15bda2f", + "local-path": "/lib/x86_64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/amd64/ruby-3.1.4p223-loop.json b/utils/coredump/testdata/amd64/ruby-3.1.4p223-loop.json new file mode 100644 index 00000000..62e23a28 --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby-3.1.4p223-loop.json @@ -0,0 +1,106 @@ +{ + "coredump-ref": "f1342370403d8c73b438d1a550205fe235de05f1bd1c65a916ccd9d0d61a5f63", + "threads": [ + { + "lwp": 18512, + "frames": [ + "libruby.so.3.1.4+0x2c8575", + "libruby.so.3.1.4+0x2cbf25", + "libruby.so.3.1.4+0x1ebcc3", + "libruby.so.3.1.4+0x2aeaeb", + "libruby.so.3.1.4+0x2b4138", + "libruby.so.3.1.4+0x2bdade", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.1.4+0x2c37b5", + "libruby.so.3.1.4+0x2c7b42", + "libruby.so.3.1.4+0x1ebb2d", + "libruby.so.3.1.4+0x2aeaeb", + "libruby.so.3.1.4+0x2b4138", + "libruby.so.3.1.4+0x2bdade", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.1.4+0x2c2ede", + "libruby.so.3.1.4+0x2c431a", + "libruby.so.3.1.4+0xddd1a", + "libruby.so.3.1.4+0xddf89", + "libruby.so.3.1.4+0x2aeaeb", + "libruby.so.3.1.4+0x2b4138", + "libruby.so.3.1.4+0x2bdade", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.1.4+0x2c2ede", + "libruby.so.3.1.4+0xda2d7", + "libruby.so.3.1.4+0xe0575", + "ruby+0x110a", + "libc-2.31.so+0x23d09", + "ruby+0x1159" + ] + } + ], + "modules": [ + { + "ref": "00dd48e97d16bd22f6c29974a5f02e44174f25474ea6f1bb0b3bdb869fcef11f", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "c1c43f528f73e170e999a4f733fce9418b71618323613a91957157d38cf337de", + "local-path": "/usr/local/lib/ruby/3.1.0/x86_64-linux/monitor.so" + }, + { + "ref": "fc274623ac8cb59e996281dd31b4f9991a608802fd8271077e92a7519924da5c", + "local-path": "/usr/local/lib/ruby/3.1.0/x86_64-linux/enc/trans/transdb.so" + }, + { + "ref": "149f28b4d4913f7b62ec5aaaf9a6f92d4964595c7f08703fe840b477626f566c", + "local-path": "/usr/local/lib/ruby/3.1.0/x86_64-linux/enc/encdb.so" + }, + { + "ref": "ad0110dd4067acf54418115178a976927cc31c894e50919270565a909e7f9aed", + "local-path": "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "e09732683527119fd0a666c88c3dec725094c20815a207be020fe68402c8b8a2", + "local-path": "/lib/x86_64-linux-gnu/libc-2.31.so" + }, + { + "ref": "5461e535328078fb72f25f7ca2bf36bc71c415bbf573ccdfa289a7b796ddfdd3", + "local-path": "/lib/x86_64-linux-gnu/libm-2.31.so" + }, + { + "ref": "cef2a912ebcad80814c5bf4cabb4a5fe2d8d680be98c9711796e0f773e5ccb06", + "local-path": "/lib/x86_64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "df5973c718b4ee540cdd4400e19e90182ab5f3bac2148c0c9f3bb92eb6af3121", + "local-path": "/lib/x86_64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "1ce1c61a3bb0eff2c58b3ff3a473972dc7d95d45fba8abaa0e5e993d923e80c2", + "local-path": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "a24ccd19ad1dc82d78d825553c4ad8332e25f879970fe602d230312d93b5ce23", + "local-path": "/lib/x86_64-linux-gnu/librt-2.31.so" + }, + { + "ref": "9807a603f7e37097a442126303e0705a85f38ad4926db8eaf5db4befad118e11", + "local-path": "/lib/x86_64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "062372f2053424d8bafd9378ebdcc29a08d7e1850342b3fd5a0e73462ed84901", + "local-path": "/lib/x86_64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "acd78e382e4d3006169b1e5539093586b101facf29f3742b42def340cd3cb4fa", + "local-path": "/usr/local/lib/libruby.so.3.1.4" + }, + { + "ref": "4325e436d7b281490848dfb580f16844adcf31afedf3972e12a660abe5c057c2", + "local-path": "/lib/x86_64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/amd64/ruby-3.2.2-loop.json b/utils/coredump/testdata/amd64/ruby-3.2.2-loop.json new file mode 100644 index 00000000..dc950c21 --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby-3.2.2-loop.json @@ -0,0 +1,98 @@ +{ + "coredump-ref": "15e3ebcfd649328d4e651f678c882ee77d8c3cf72bd1e311fad16dca98ec7ab1", + "threads": [ + { + "lwp": 9454, + "frames": [ + "libruby.so.3.2.2+0x345fbe", + "libruby.so.3.2.2+0x25cc83", + "libruby.so.3.2.2+0x3273c6", + "libruby.so.3.2.2+0x32d573", + "libruby.so.3.2.2+0x337f4f", + "is_prime+0 in /pwd/utils/coredump/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/utils/coredump/testsources/ruby/loop.rb:20", + "
+0 in /pwd/utils/coredump/testsources/ruby/loop.rb:30", + "libruby.so.3.2.2+0x33cfbb", + "libruby.so.3.2.2+0x34144d", + "libruby.so.3.2.2+0x25c9dd", + "libruby.so.3.2.2+0x3273c6", + "libruby.so.3.2.2+0x32d573", + "libruby.so.3.2.2+0x337f4f", + "
+0 in /pwd/utils/coredump/testsources/ruby/loop.rb:29", + "libruby.so.3.2.2+0x33cb52", + "libruby.so.3.2.2+0x33e1d2", + "libruby.so.3.2.2+0x155f9a", + "libruby.so.3.2.2+0x156209", + "libruby.so.3.2.2+0x3273c6", + "libruby.so.3.2.2+0x32d573", + "libruby.so.3.2.2+0x337f4f", + "
+0 in /pwd/utils/coredump/testsources/ruby/loop.rb:28", + "libruby.so.3.2.2+0x33cb52", + "libruby.so.3.2.2+0x151cb4", + "libruby.so.3.2.2+0x15817a", + "ruby+0x1111", + "libc.so.6+0x271c9", + "libc.so.6+0x27284", + "ruby+0x1150" + ] + } + ], + "modules": [ + { + "ref": "e8c273d91b7af9e2700183fef0fa7b14b9166f83569f3d58ff1e65a3aea2a6a2", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "a9c19d6beae1af4599194b0a30f9fd4196e83403fee86f45a64855c98c75d76f", + "local-path": "/usr/local/lib/ruby/3.2.0/x86_64-linux/monitor.so" + }, + { + "ref": "d6bcb346ad5a72abbdbe413503350fc8baf71fcff8325fb3e25ad70e26814903", + "local-path": "/usr/local/lib/ruby/3.2.0/x86_64-linux/enc/trans/transdb.so" + }, + { + "ref": "52c227df9d53248238602c1ddaccd2c8ddc4cc6a61aa45d7c425af590b8806a5", + "local-path": "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e4b5576b19e40be5923b0eb864750d35944404bb0a92aa68d1a9b96110c52120", + "local-path": "/usr/lib/locale/C.utf8/LC_CTYPE" + }, + { + "ref": "8b7e66a8f391da9240ea76f9a7863fc1beeca38eaf308ab509677ef19d3aaad0", + "local-path": "/usr/lib/x86_64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "deb95477124da0f3a4b45365e03351ccf0488b9c400acba99ac2424498fa014e", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "c6ed53906027691e7e29af7f0fb1c5c740b3ba47fc746dc4863944441231b5dd", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "5db18e8a8894ef4746eb8230855b638a5e52e782b2f10deede5f1dad846178bb", + "local-path": "/usr/lib/x86_64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "7376c9af0afd6e7698a64ee19de3c8a0199418664974384c70435a51c7ff7f3f", + "local-path": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "7e2a72b4c4b38c61e6962de6e3f4a5e9ae692e732c68deead10a7ce2135a7f68", + "local-path": "/usr/lib/x86_64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "c7a1c559f89695d0efca87c1c2497fb51cf4e2f731dfc1e9077c5d033e78be6b", + "local-path": "/usr/local/lib/ruby/3.2.0/x86_64-linux/enc/encdb.so" + }, + { + "ref": "07d6c69e220ac3f9f1edda72c7481ca9c9245e470dc20abc24538f22cd3ee37d", + "local-path": "/usr/local/lib/libruby.so.3.2.2" + }, + { + "ref": "038f15428b7acbd824f49c83306b7c1c0f96d37d1fef1d1c0913848fa21f2c9f", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + } + ] +} diff --git a/utils/coredump/testdata/amd64/ruby25.20836.json b/utils/coredump/testdata/amd64/ruby25.20836.json new file mode 100644 index 00000000..6897f34a --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby25.20836.json @@ -0,0 +1,50 @@ +{ + "coredump-ref": "a52c68c34e13a52931d41619d965b875128e6540376e991f93c2a5bc5f0d9951", + "threads": [ + { + "lwp": 20836, + "frames": [ + "libruby-2.5.so.2.5.5+0xaafc4", + "libruby-2.5.so.2.5.5+0xadd73", + "libruby-2.5.so.2.5.5+0x183762", + "libruby-2.5.so.2.5.5+0xef995", + "libruby-2.5.so.2.5.5+0x127be1", + "libruby-2.5.so.2.5.5+0x1b8c0c", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1c4e88", + "libruby-2.5.so.2.5.5+0x1264d7", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1c238a", + "libruby-2.5.so.2.5.5+0x1b9a59", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1bf4b2", + "libruby-2.5.so.2.5.5+0x97601", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1c238a", + "libruby-2.5.so.2.5.5+0x1b9a59", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x94f13", + "is_prime+0 in /tmp/systemtest_sum_of_primes_1619527295051925477.rb:10", + "sum_of_primes+0 in /tmp/systemtest_sum_of_primes_1619527295051925477.rb:20", + "
+0 in /tmp/systemtest_sum_of_primes_1619527295051925477.rb:30", + "
+0 in /tmp/systemtest_sum_of_primes_1619527295051925477.rb:29", + "
+0 in /tmp/systemtest_sum_of_primes_1619527295051925477.rb:28", + "libruby-2.5.so.2.5.5+0x96dbc", + "libruby-2.5.so.2.5.5+0x9926d", + "ruby2.5+0x10ea", + "libc-2.28.so+0x2409a", + "ruby2.5+0x1119" + ] + }, + { + "lwp": 20837, + "frames": [ + "libc-2.28.so+0xee819", + "libruby-2.5.so.2.5.5+0x190133", + "libpthread-2.28.so+0x7fa2", + "libc-2.28.so+0xf94ce" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/ruby25.benchmark-serialization.10811.json b/utils/coredump/testdata/amd64/ruby25.benchmark-serialization.10811.json new file mode 100644 index 00000000..e0488d66 --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby25.benchmark-serialization.10811.json @@ -0,0 +1,104 @@ +{ + "coredump-ref": "9ac82f530b4977351b81b92b732e59e29509b304496de35bc5d86aac82c2135a", + "threads": [ + { + "lwp": 10811, + "frames": [ + "parser.so+0x3489", + "parser.so+0x3993", + "parser.so+0x3f9b", + "parser.so+0x465b", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1ba305", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1c4e88", + "libruby-2.5.so.2.5.5+0xe19bd", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1b9a59", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1bfb28", + "libruby-2.5.so.2.5.5+0xe262f", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1c238a", + "libruby-2.5.so.2.5.5+0x1b9a59", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1c0f0c", + "libruby-2.5.so.2.5.5+0x1c1009", + "libruby-2.5.so.2.5.5+0x1c1353", + "libruby-2.5.so.2.5.5+0x1ba305", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1c4e88", + "libruby-2.5.so.2.5.5+0x2f4cb", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1b9a59", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1c4e88", + "libruby-2.5.so.2.5.5+0x2f4cb", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1c238a", + "libruby-2.5.so.2.5.5+0x1b9a59", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x1c4e88", + "libruby-2.5.so.2.5.5+0x2f4cb", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1c238a", + "libruby-2.5.so.2.5.5+0x1b9a59", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0xd0a40", + "libruby-2.5.so.2.5.5+0xd0ff7", + "libruby-2.5.so.2.5.5+0xd110f", + "libruby-2.5.so.2.5.5+0x1b47a0", + "libruby-2.5.so.2.5.5+0x1c238a", + "libruby-2.5.so.2.5.5+0x1ba305", + "libruby-2.5.so.2.5.5+0x1bdef3", + "libruby-2.5.so.2.5.5+0x94f13", + "parse+0 in /usr/lib/ruby/vendor_ruby/json/common.rb:156", + "run+0 in /tmp/puppet/benchmarks/serialization/benchmarker.rb:48", + "run+0 in /tmp/puppet/benchmarks/serialization/benchmarker.rb:28", + "generate_scenario_tasks+0 in /tmp/puppet/tasks/benchmark.rake:47", + "measure+0 in /usr/lib/ruby/2.5.0/benchmark.rb:293", + "item+0 in /usr/lib/ruby/2.5.0/benchmark.rb:375", + "generate_scenario_tasks+0 in /tmp/puppet/tasks/benchmark.rake:46", + "generate_scenario_tasks+0 in /tmp/puppet/tasks/benchmark.rake:44", + "benchmark+0 in /usr/lib/ruby/2.5.0/benchmark.rb:173", + "generate_scenario_tasks+0 in /tmp/puppet/tasks/benchmark.rake:42", + "execute+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:281", + "execute+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:281", + "invoke_with_call_chain+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:219", + "mon_synchronize+0 in /usr/lib/ruby/2.5.0/monitor.rb:226", + "invoke_with_call_chain+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:199", + "invoke_prerequisites+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:243", + "invoke_prerequisites+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:241", + "invoke_with_call_chain+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:218", + "mon_synchronize+0 in /usr/lib/ruby/2.5.0/monitor.rb:226", + "invoke_with_call_chain+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:199", + "invoke+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/task.rb:188", + "invoke_task+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:160", + "top_level+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:116", + "top_level+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:116", + "run_with_threads+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:125", + "top_level+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:110", + "run+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:83", + "standard_exception_handling+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:186", + "run+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/lib/rake/application.rb:80", + "+0 in /var/lib/gems/2.5.0/gems/rake-13.0.6/exe/rake:27", + "
+0 in /usr/local/bin/rake:23", + "libruby-2.5.so.2.5.5+0x96dbc", + "libruby-2.5.so.2.5.5+0x9926d", + "ruby2.5+0x10ea", + "libc-2.28.so+0x2409a", + "ruby2.5+0x1119" + ] + }, + { + "lwp": 10814, + "frames": [ + "libc-2.28.so+0xee819", + "libruby-2.5.so.2.5.5+0x190133", + "libpthread-2.28.so+0x7fa2", + "libc-2.28.so+0xf94ce" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/ruby27.2186.json b/utils/coredump/testdata/amd64/ruby27.2186.json new file mode 100644 index 00000000..dacfd6e7 --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby27.2186.json @@ -0,0 +1,42 @@ +{ + "coredump-ref": "8db1adbdbd52401507731d6f8cd3617f01e961a345c332ab6d7d16d3bcbd5612", + "threads": [ + { + "lwp": 2186, + "frames": [ + "libruby-2.7.so.2.7.2+0xc54b8", + "libruby-2.7.so.2.7.2+0xc6e77", + "libruby-2.7.so.2.7.2+0xc8ce1", + "libruby-2.7.so.2.7.2+0x1dde73", + "libruby-2.7.so.2.7.2+0x13bef3", + "libruby-2.7.so.2.7.2+0x180aa0", + "libruby-2.7.so.2.7.2+0x220e5b", + "is_prime+0 in /tmp/systemtest_sum_of_primes_1619510622181218920.rb:10", + "sum_of_primes+0 in /tmp/systemtest_sum_of_primes_1619510622181218920.rb:20", + "
+0 in /tmp/systemtest_sum_of_primes_1619510622181218920.rb:30", + "libruby-2.7.so.2.7.2+0x22623a", + "libruby-2.7.so.2.7.2+0x2326e4", + "libruby-2.7.so.2.7.2+0x17fc8d", + "libruby-2.7.so.2.7.2+0x21bf71", + "libruby-2.7.so.2.7.2+0x210745", + "libruby-2.7.so.2.7.2+0x22075a", + "
+0 in /tmp/systemtest_sum_of_primes_1619510622181218920.rb:29", + "libruby-2.7.so.2.7.2+0x22623a", + "libruby-2.7.so.2.7.2+0x229141", + "libruby-2.7.so.2.7.2+0xadb26", + "libruby-2.7.so.2.7.2+0xadd39", + "libruby-2.7.so.2.7.2+0x21bf71", + "libruby-2.7.so.2.7.2+0x210745", + "libruby-2.7.so.2.7.2+0x22075a", + "
+0 in /tmp/systemtest_sum_of_primes_1619510622181218920.rb:28", + "libruby-2.7.so.2.7.2+0x22623a", + "libruby-2.7.so.2.7.2+0xab69f", + "libruby-2.7.so.2.7.2+0xafab7", + "ruby2.7+0x110a", + "libc-2.31.so+0x26d09", + "ruby2.7+0x1159" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/ruby30.253432.json b/utils/coredump/testdata/amd64/ruby30.253432.json new file mode 100644 index 00000000..08e59ee8 --- /dev/null +++ b/utils/coredump/testdata/amd64/ruby30.253432.json @@ -0,0 +1,44 @@ +{ + "coredump-ref": "ff1183a2679a72cdd32b3963af31bbf0aa3067c6e713cd02e6722015673e0633", + "threads": [ + { + "lwp": 253432, + "frames": [ + "libruby.so.3.0.2+0x249714", + "libruby.so.3.0.2+0x24aa77", + "libruby.so.3.0.2+0x194bec", + "libruby.so.3.0.2+0x195d12", + "libruby.so.3.0.2+0x1983e1", + "libruby.so.3.0.2+0x23bd74", + "libruby.so.3.0.2+0x23e0d5", + "libruby.so.3.0.2+0x240db6", + "is_prime+0 in /home/flehner/tmp/ruby3/loop.rb:10", + "sum_of_primes+0 in /home/flehner/tmp/ruby3/loop.rb:20", + "
+0 in /home/flehner/tmp/ruby3/loop.rb:30", + "libruby.so.3.0.2+0x2461e4", + "libruby.so.3.0.2+0x24aa77", + "libruby.so.3.0.2+0x1982dd", + "libruby.so.3.0.2+0x23bd74", + "libruby.so.3.0.2+0x23e0d5", + "libruby.so.3.0.2+0x240db6", + "
+0 in /home/flehner/tmp/ruby3/loop.rb:29", + "libruby.so.3.0.2+0x2461e4", + "libruby.so.3.0.2+0x249387", + "libruby.so.3.0.2+0xb2e49", + "libruby.so.3.0.2+0xb30ad", + "libruby.so.3.0.2+0x23bd74", + "libruby.so.3.0.2+0x23e0d5", + "libruby.so.3.0.2+0x240db6", + "
+0 in /home/flehner/tmp/ruby3/loop.rb:28", + "libruby.so.3.0.2+0x2461e4", + "libruby.so.3.0.2+0xb1c1a", + "libruby.so.3.0.2+0xb54f9", + "ruby-mri+0x118e", + "libc.so.6+0x2d55f", + "libc.so.6+0x2d60b", + "ruby-mri+0x11d4" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/stackalign.4040.json b/utils/coredump/testdata/amd64/stackalign.4040.json new file mode 100644 index 00000000..afe2124b --- /dev/null +++ b/utils/coredump/testdata/amd64/stackalign.4040.json @@ -0,0 +1,19 @@ +{ + "coredump-ref": "2bf8aa05076f4f1b8ce7d7cfe0174477229164f7b3e59a4ffc0c1a6bbc392926", + "threads": [ + { + "lwp": 4040, + "frames": [ + "libc-2.31.so+0xcb3c3", + "stackalign+0x12b0", + "stackalign+0x12be", + "stackalign+0x12be", + "stackalign+0x12be", + "stackalign+0x106a", + "libc-2.31.so+0x26d09", + "stackalign+0x10a9" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/stackdeltas.11629.json b/utils/coredump/testdata/amd64/stackdeltas.11629.json new file mode 100644 index 00000000..378121db --- /dev/null +++ b/utils/coredump/testdata/amd64/stackdeltas.11629.json @@ -0,0 +1,216 @@ +{ + "coredump-ref": "eefd21e13d21b6b0c358093707fb9406c1f2eb2bed0de8ced3add545f3c822ff", + "threads": [ + { + "lwp": 11638, + "frames": [ + "stackdeltas+0x469841", + "stackdeltas+0x44b05c", + "stackdeltas+0x44b570", + "stackdeltas+0x449ed3", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469840", + "stackdeltas+0x44b05c", + "stackdeltas+0x44b219", + "stackdeltas+0x44a83a", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43b7b8", + "stackdeltas+0x43ceb1", + "stackdeltas+0x43e3ed", + "stackdeltas+0x43f736", + "stackdeltas+0x43fcbc", + "stackdeltas+0x4661fa" + ] + }, + { + "lwp": 11630, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e42", + "stackdeltas+0x432616", + "stackdeltas+0x40d6d8", + "stackdeltas+0x40d7b0", + "stackdeltas+0x444524", + "stackdeltas+0x43b6c7", + "stackdeltas+0x43b5cd" + ] + }, + { + "lwp": 11633, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43b7b8", + "stackdeltas+0x43ceb1", + "stackdeltas+0x43e3ed", + "stackdeltas+0x43f736", + "stackdeltas+0x43fcbc", + "stackdeltas+0x4661fa" + ] + }, + { + "lwp": 11631, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x4f7761", + "stackdeltas+0x4eebf2", + "stackdeltas+0x4effd0", + "stackdeltas+0x4f6937", + "stackdeltas+0x4f6be6", + "stackdeltas+0x4eb2da", + "stackdeltas+0x4eb230", + "stackdeltas+0x467ccf", + "stackdeltas+0x462784", + "stackdeltas+0x4e9b87", + "stackdeltas+0x4e9c84", + "stackdeltas+0x4e94f3", + "stackdeltas+0x4e9084", + "stackdeltas+0x4ea393", + "stackdeltas+0x4eafc4", + "stackdeltas+0x438ad5", + "stackdeltas+0x468060" + ] + }, + { + "lwp": 11629, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43b7b8", + "stackdeltas+0x43ceb1", + "stackdeltas+0x43e3ed", + "stackdeltas+0x43f736", + "stackdeltas+0x43fcbc", + "stackdeltas+0x4661fa" + ] + }, + { + "lwp": 11634, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43cdf9", + "stackdeltas+0x43b6c7", + "stackdeltas+0x43b5cd" + ] + }, + { + "lwp": 11632, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43b7b8", + "stackdeltas+0x43ceb1", + "stackdeltas+0x43e3ed", + "stackdeltas+0x43f736", + "stackdeltas+0x43fcbc", + "stackdeltas+0x4661fa" + ] + }, + { + "lwp": 11635, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43b7b8", + "stackdeltas+0x43ceb1", + "stackdeltas+0x43e3ed", + "stackdeltas+0x43f736", + "stackdeltas+0x43fcbc", + "stackdeltas+0x4661fa" + ] + }, + { + "lwp": 11636, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43b7b8", + "stackdeltas+0x43ceb1", + "stackdeltas+0x43e3ed", + "stackdeltas+0x43f736", + "stackdeltas+0x43fcbc", + "stackdeltas+0x4661fa" + ] + }, + { + "lwp": 11637, + "frames": [ + "stackdeltas+0x4697dd", + "stackdeltas+0x44a891", + "stackdeltas+0x44a891", + "stackdeltas+0x44a044", + "stackdeltas+0x469ba2", + "ld-musl-x86_64.so.1+0x46c4c", + "stackdeltas+0x469e40", + "stackdeltas+0x4325a5", + "stackdeltas+0x40d53e", + "stackdeltas+0x43b7b8", + "stackdeltas+0x43ceb1", + "stackdeltas+0x43e3ed", + "stackdeltas+0x43f736", + "stackdeltas+0x43fcbc", + "stackdeltas+0x4661fa" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/.gitkeep b/utils/coredump/testdata/arm64/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/utils/coredump/testdata/arm64/brokenstack.json b/utils/coredump/testdata/arm64/brokenstack.json new file mode 100644 index 00000000..942536b2 --- /dev/null +++ b/utils/coredump/testdata/arm64/brokenstack.json @@ -0,0 +1,28 @@ +{ + "coredump-ref": "970f0555ecdcb052c566a4502e0889c3af21b1b83ba69dc30c9bc21f7cd2003b", + "threads": [ + { + "lwp": 32071, + "frames": [ + "brokenstack+0x768", + "brokenstack+0x77f", + "brokenstack+0x7a7", + "" + ] + } + ], + "modules": [ + { + "ref": "9737f2a0aa44a493f1078879a4a5a45f8669c21c4d24b0f7666417888bbeff5b", + "local-path": "/media/share/Development/prodfiler/utils/coredump/testsources/c/brokenstack" + }, + { + "ref": "d985b3f544f1d4ff3c152004dafab112269eb9b5424723e02ff1f2b65ef39825", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + }, + { + "ref": "de247820b5edcc25519996cbefa697459ff0a64ff4d2a5a2e1b7b3c468c3da3e", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/glibc-signal-arm.json b/utils/coredump/testdata/arm64/glibc-signal-arm.json new file mode 100644 index 00000000..acd2ab5e --- /dev/null +++ b/utils/coredump/testdata/arm64/glibc-signal-arm.json @@ -0,0 +1,36 @@ +{ + "coredump-ref": "6a7fdbf45a7cc74b556668eea4bc54310bcc12d5b6fbd8bef87235d6c1c1e021", + "threads": [ + { + "lwp": 17849, + "frames": [ + "libc.so.6+0xc090c", + "libc.so.6+0xc5c5f", + "libc.so.6+0xc5b0f", + "sig+0x4101f7", + "linux-vdso.1.so+0x803", + "libc.so.6+0xc090f", + "libc.so.6+0xc5c5f", + "libc.so.6+0xc5b0f", + "sig+0x410237", + "libc.so.6+0x35a73", + "libc.so.6+0x35b5b", + "sig+0x4100ef" + ] + } + ], + "modules": [ + { + "ref": "0fdfedf4db555c77c21e4e942b747963b08f35890fe61c4562e2e1a07853e559", + "local-path": "/home/ec2-user/sig" + }, + { + "ref": "7436f95b8689a98ab322c19f96621da984d7062555107b55ac42d03303d9eaaa", + "local-path": "/usr/lib64/libc.so.6" + }, + { + "ref": "20b53776e9fc8e7263c1911b7e338119c777212ffe6028cfe3e524796e403736", + "local-path": "/usr/lib/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/go.symbhack.readheader.json b/utils/coredump/testdata/arm64/go.symbhack.readheader.json new file mode 100644 index 00000000..e6c0575d --- /dev/null +++ b/utils/coredump/testdata/arm64/go.symbhack.readheader.json @@ -0,0 +1,166 @@ +{ + "coredump-ref": "cc5cd2ce10d88f6ebdf19434e2b8a8d0882feab078ed0c04c679b24859c740d1", + "threads": [ + { + "lwp": 243879, + "frames": [ + "symbhack+0x575ee4", + "symbhack+0x5708b3", + "symbhack+0x570343", + "symbhack+0x825ed7", + "symbhack+0x826507", + "symbhack+0x822fc7", + "symbhack+0xf8fb27", + "symbhack+0xf8f2eb", + "symbhack+0xf8e6e3", + "symbhack+0x447677", + "symbhack+0x477ac3" + ] + }, + { + "lwp": 243876, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44a1df", + "symbhack+0x44b7df", + "symbhack+0x44c31b", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + }, + { + "lwp": 243878, + "frames": [ + "symbhack+0x478914", + "symbhack+0x451f07", + "symbhack+0x44a0e7", + "symbhack+0x44a03b", + "symbhack+0x4753df" + ] + }, + { + "lwp": 243880, + "frames": [ + "symbhack+0x47915c", + "symbhack+0x4410c7", + "symbhack+0x44c9d7", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + }, + { + "lwp": 243881, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44a1df", + "symbhack+0x44b7df", + "symbhack+0x44c31b", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + }, + { + "lwp": 243882, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44b6e7", + "symbhack+0x44a0e7", + "symbhack+0x44a03b", + "symbhack+0x4753df" + ] + }, + { + "lwp": 243883, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44a1df", + "symbhack+0x44b7df", + "symbhack+0x44c31b", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + }, + { + "lwp": 243884, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44a1df", + "symbhack+0x44b7df", + "symbhack+0x44c31b", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + }, + { + "lwp": 243885, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44a1df", + "symbhack+0x44b7df", + "symbhack+0x44c31b", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + }, + { + "lwp": 243886, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44a1df", + "symbhack+0x44b7df", + "symbhack+0x44c31b", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + }, + { + "lwp": 243887, + "frames": [ + "symbhack+0x478fcc", + "symbhack+0x44132b", + "symbhack+0x416daf", + "symbhack+0x44a1df", + "symbhack+0x44b7df", + "symbhack+0x44c31b", + "symbhack+0x44daaf", + "symbhack+0x44dfcb", + "symbhack+0x475453" + ] + } + ], + "modules": [ + { + "ref": "d2d4a688141a686667220fa84ec8b4c9c4ec805c00c9a1f03ce0b8a45fa6c2e6", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + }, + { + "ref": "8a15b787643d08b76e8ab7b7f67d614282af6cf310c9cf20af8ede95a668c621", + "local-path": "/media/psf/devel/prodfiler/utils/symbhack/symbhack" + }, + { + "ref": "acf9cd2b988c12b0e9ba02cb457b5fc81ffab1b4683ac166edbced3333fe5482", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json b/utils/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json new file mode 100644 index 00000000..0111f5f6 --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json @@ -0,0 +1,72 @@ +{ + "coredump-ref": "c5cafafe70f6a5aa9513c11e6606ad1a5ff2396a636766e6fc05b3c0c132a9d3", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90c94", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d324", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello4.epi.add.json b/utils/coredump/testdata/arm64/hello.3345.hello4.epi.add.json new file mode 100644 index 00000000..81601ec0 --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello4.epi.add.json @@ -0,0 +1,73 @@ +{ + "coredump-ref": "3cc93296b2e5df90d4272ad62db48c7b24ed7842034a6daa36586937b508a880", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90d90", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d324", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json b/utils/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json new file mode 100644 index 00000000..7709c001 --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json @@ -0,0 +1,73 @@ +{ + "coredump-ref": "7676bb33e4f72ce24349263c8a2f6a664d499f8265801236cb0c2e9b3f3d6954", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90d94", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d324", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json b/utils/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json new file mode 100644 index 00000000..962400cb --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json @@ -0,0 +1,74 @@ +{ + "coredump-ref": "a05f715f5ac0cd5f1a3521badd8d0ccd13b385819471712399e0b5e785fa5453", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90dcc", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d328", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.epi.add.json b/utils/coredump/testdata/arm64/hello.3345.hello5.epi.add.json new file mode 100644 index 00000000..dbbe9c1e --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.epi.add.json @@ -0,0 +1,87 @@ +{ + "coredump-ref": "3c58ef393be685a2abf413e78c1120aa1414ec86d2936dc69ae1ce5b033aee38", + "threads": [ + { + "lwp": 24601, + "frames": [ + "hello.3345+0x90e10", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 24603, + "frames": [ + "hello.3345+0x6d324", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 24604, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 24606, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 24605, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 24607, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json b/utils/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json new file mode 100644 index 00000000..5ab86595 --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json @@ -0,0 +1,87 @@ +{ + "coredump-ref": "b92257b70bc5948536802b60390b411e8fc75e6af318f058f2026efb06d268bf", + "threads": [ + { + "lwp": 24601, + "frames": [ + "hello.3345+0x90e14", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 24603, + "frames": [ + "hello.3345+0x6d324", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 24604, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 24606, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 24605, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 24607, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.add.json b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.add.json new file mode 100644 index 00000000..b36c968c --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.add.json @@ -0,0 +1,74 @@ +{ + "coredump-ref": "6052135419cc13622d0a49db49542e00352169234add3c460eb814870c1302cc", + "threads": [ + { + "lwp": 155895, + "frames": [ + "hello.3345+0x90dd0", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 155900, + "frames": [ + "hello.3345+0x6d328", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 155901, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 155902, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 155903, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json new file mode 100644 index 00000000..c633739a --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json @@ -0,0 +1,74 @@ +{ + "coredump-ref": "4a587aea8e7e51f8a1f2e9c441eb31ba9bc12eab5689ec99524c85a92034bd08", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90dc8", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d328", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.str.json b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.str.json new file mode 100644 index 00000000..40d21f17 --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.str.json @@ -0,0 +1,74 @@ +{ + "coredump-ref": "699e2c3340094b69b6040c28d329b9c13b356963df63375bbea15fba83724810", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90dbc", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d328", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json new file mode 100644 index 00000000..81d1c3ce --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json @@ -0,0 +1,74 @@ +{ + "coredump-ref": "77ca3d48ef63837014f381a9e7986bcd6d95bd87856f7c40faaf1a55039a063a", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90dc0", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d324", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json new file mode 100644 index 00000000..ef78f878 --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json @@ -0,0 +1,74 @@ +{ + "coredump-ref": "25f94a50c52e836a1ee0b38ca31342e06e8bea685b6a64fd0e326d7ec30eb3c6", + "threads": [ + { + "lwp": 3662, + "frames": [ + "hello.3345+0x90dc4", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 3664, + "frames": [ + "hello.3345+0x6d328", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 3665, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3666, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 3667, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.3345.leaf.ret.json b/utils/coredump/testdata/arm64/hello.3345.leaf.ret.json new file mode 100644 index 00000000..ac87a7cd --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.3345.leaf.ret.json @@ -0,0 +1,75 @@ +{ + "coredump-ref": "6505deb8d72f6a1751c36d092d62f785289b03854ffe1638522b657d6504106e", + "threads": [ + { + "lwp": 156629, + "frames": [ + "hello.3345+0x90a90", + "hello.3345+0x90e0b", + "hello.3345+0x90d4b", + "hello.3345+0x90c93", + "hello.3345+0x90c43", + "hello.3345+0x90b3b", + "hello.3345+0x90afb", + "hello.3345+0x90e4b", + "hello.3345+0x43533", + "hello.3345+0x6c513" + ] + }, + { + "lwp": 156634, + "frames": [ + "hello.3345+0x6d324", + "hello.3345+0x4eec7", + "hello.3345+0x46237", + "hello.3345+0x46187", + "hello.3345+0x69f5f" + ] + }, + { + "lwp": 156635, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x485db", + "hello.3345+0x4a363", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 156636, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + }, + { + "lwp": 156637, + "frames": [ + "hello.3345+0x6d9bc", + "hello.3345+0x3d6fb", + "hello.3345+0x1993f", + "hello.3345+0x47b13", + "hello.3345+0x4938b", + "hello.3345+0x4a3a7", + "hello.3345+0x4a8ff", + "hello.3345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "bb87b9bba238372aad8a65bacbf088583cf7a580daa23abfed2467a19307c707", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.3345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/hello.345.hello5.body.add.json b/utils/coredump/testdata/arm64/hello.345.hello5.body.add.json new file mode 100644 index 00000000..2aecd586 --- /dev/null +++ b/utils/coredump/testdata/arm64/hello.345.hello5.body.add.json @@ -0,0 +1,61 @@ +{ + "coredump-ref": "9ab16c3f657ae5d69b0ba37256d9fec367c2da02973be36b6b9bfed743253feb", + "threads": [ + { + "lwp": 155034, + "frames": [ + "hello.345+0x90db0", + "hello.345+0x90d2b", + "hello.345+0x90c73", + "hello.345+0x90c2b", + "hello.345+0x90b3b", + "hello.345+0x90afb", + "hello.345+0x90e2b", + "hello.345+0x43533", + "hello.345+0x6c513" + ] + }, + { + "lwp": 155058, + "frames": [ + "hello.345+0x6d324", + "hello.345+0x4eec7", + "hello.345+0x46237", + "hello.345+0x46187", + "hello.345+0x69f5f" + ] + }, + { + "lwp": 155059, + "frames": [ + "hello.345+0x6d9bc", + "hello.345+0x3d6fb", + "hello.345+0x1993f", + "hello.345+0x47b13", + "hello.345+0x4938b", + "hello.345+0x4a3a7", + "hello.345+0x4a8ff", + "hello.345+0x69fd3" + ] + }, + { + "lwp": 155060, + "frames": [ + "hello.345+0x6d9bc", + "hello.345+0x3d6fb", + "hello.345+0x1993f", + "hello.345+0x47b13", + "hello.345+0x4938b", + "hello.345+0x4a3a7", + "hello.345+0x4a8ff", + "hello.345+0x69fd3" + ] + } + ], + "modules": [ + { + "ref": "ff1d01d8c89db3cbadbd6c17b3c2dce526002e157ea60ec674876b35afc8910e", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.345" + } + ] +} diff --git a/utils/coredump/testdata/arm64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h.json b/utils/coredump/testdata/arm64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h.json new file mode 100644 index 00000000..280f569b --- /dev/null +++ b/utils/coredump/testdata/arm64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h.json @@ -0,0 +1,357 @@ +{ + "coredump-ref": "b815b983b62d0b2577d06918b1bc55d57649e8683cfc15eec2661e52564aefd8", + "threads": [ + { + "lwp": 91977, + "frames": [ + "StubRoutines (2) [sha256_implCompressMB]+0 in :0", + "byte[] ShaShenanigans.hashRandomStuff()+3 in ShaShenanigans.java:29", + "void ShaShenanigans.shaShenanigans()+2 in ShaShenanigans.java:20", + "void ShaShenanigans.main(java.lang.String[])+0 in ShaShenanigans.java:13", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7cf0cb", + "libjvm.so+0x85f38b", + "libjvm.so+0x86182f", + "libjli.so+0x4037", + "libjli.so+0x6f1b", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91975, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x806d7", + "libjli.so+0x7a5b", + "libjli.so+0x5193", + "libjli.so+0x629b", + "java+0xacf", + "libc.so.6+0x2777f", + "libc.so.6+0x27857", + "java+0xb6f" + ] + }, + { + "lwp": 91978, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x8732b", + "libjvm.so+0xbfe27b", + "libjvm.so+0xe59ff7", + "libjvm.so+0xe5a08f", + "" + ] + }, + { + "lwp": 91979, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xb60ff7", + "libjvm.so+0xb15217", + "libjvm.so+0x6b70f7", + "libjvm.so+0x5b77cb", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91980, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x8732b", + "libjvm.so+0xbfe27b", + "libjvm.so+0xe59ff7", + "libjvm.so+0xe5a08f", + "" + ] + }, + { + "lwp": 91981, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x8732b", + "libjvm.so+0xbfe27b", + "libjvm.so+0x6b89b7", + "libjvm.so+0x5b77cb", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91982, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60f53", + "libjvm.so+0xb15217", + "libjvm.so+0x70a383", + "libjvm.so+0x70a5a3", + "libjvm.so+0x5b77cb", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91983, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60f53", + "libjvm.so+0xb15217", + "libjvm.so+0xe3fca3", + "libjvm.so+0xe408db", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91984, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xb60ff7", + "libjvm.so+0xb152b3", + "libjvm.so+0x8905a7", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7cf0cb", + "libjvm.so+0x7d042b", + "libjvm.so+0x88863b", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91985, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xb6063b", + "libjvm.so+0xb35a87", + "libjvm.so+0xd7e7b7", + "libjvm.so+0x88974b", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7cf0cb", + "libjvm.so+0x7d042b", + "libjvm.so+0x88863b", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91986, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x8732b", + "libjvm.so+0xbfe27b", + "libjvm.so+0xceea73", + "libjvm.so+0xb4b30b", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91987, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xb60ff7", + "libjvm.so+0xb15217", + "libjvm.so+0xbfeccb", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91988, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60f53", + "libjvm.so+0xb15217", + "libjvm.so+0xb08c1f", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91989, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60f53", + "libjvm.so+0xb152b3", + "libjvm.so+0x59e33b", + "libjvm.so+0x5a0b0b", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91990, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60f53", + "libjvm.so+0xb152b3", + "libjvm.so+0x59e33b", + "libjvm.so+0x5a0b0b", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91991, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60f53", + "libjvm.so+0xb15217", + "libjvm.so+0xd73f33", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91992, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xb60ff7", + "libjvm.so+0xb15217", + "libjvm.so+0xb2a18b", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91993, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60f53", + "libjvm.so+0xb15217", + "libjvm.so+0xb29c2b", + "libjvm.so+0xb29d07", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 91994, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xb60817", + "libjvm.so+0xb35833", + "libjvm.so+0xd7e7b7", + "libjvm.so+0x88974b", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7cf0cb", + "libjvm.so+0x7d042b", + "libjvm.so+0x88863b", + "libjvm.so+0xdbefb7", + "libjvm.so+0xdc3dcb", + "libjvm.so+0xb56fbb", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + } + ], + "modules": [ + { + "ref": "72eb811bb6d61300f62f9e9e4d824759d3c6a8f510d157999d8d2adb6dc683d8", + "local-path": "/usr/lib/aarch64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "ae6bd25b1f9616e37fb652d1052af984576d22adacfd3bced9df82d075ad92c7", + "local-path": "/usr/lib/aarch64-linux-gnu/libm.so.6" + }, + { + "ref": "ff608307ebc72d45b92578b3eaaa0430b133c4a6625a2ed4088f8ea921973b4f", + "local-path": "/usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.32" + }, + { + "ref": "7870932cbadbac22b3df41b05d97050ca8a9a007b95c244678d7c6a9fe16d383", + "local-path": "/usr/lib/jvm/java-17-openjdk-arm64/bin/java" + }, + { + "ref": "4f5bcc96203b9c8287db6272082ec52c5f91d0d61b321cfbc0e5942154e66199", + "local-path": "/usr/lib/jvm/java-17-openjdk-arm64/lib/libjli.so" + }, + { + "ref": "b4091d46467304ba9420143b85651b94809bc99fabf676c88429235950411aba", + "local-path": "/usr/lib/jvm/java-17-openjdk-arm64/lib/server/libjvm.so" + }, + { + "ref": "ffb1ab496e6eced03ab679075f9f2c415c7728a145cc7f63d614497102d73822", + "local-path": "/usr/lib/aarch64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "95e19b060bda762e296a7b89f1f487f0516de5ae9756d42ed36179de31898169", + "local-path": "/usr/lib/jvm/java-17-openjdk-arm64/lib/libjava.so" + }, + { + "ref": "ae162a6a4dfedc12d2cdbebdecf62f24ca971ee0771d2b52bd5d2fd96a941b93", + "local-path": "/usr/lib/jvm/java-17-openjdk-arm64/lib/libjimage.so" + }, + { + "ref": "ae89390ee9874d9b0c4f7eda0ba8c798d631df0e600549d6a15f7237f03c0317", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + }, + { + "ref": "388929059d814ffeedda0beaff53722c24c59838817324d95221c18ed851f21c", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/java-21.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h2.json b/utils/coredump/testdata/arm64/java-21.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h2.json new file mode 100644 index 00000000..26e40b26 --- /dev/null +++ b/utils/coredump/testdata/arm64/java-21.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h2.json @@ -0,0 +1,359 @@ +{ + "coredump-ref": "c5256ec86b2b16cd016c16ac59a4c85d422c352ea96351428233bbd25c3915d6", + "threads": [ + { + "lwp": 121748, + "frames": [ + "StubRoutines (compiler stubs) [sha256_implCompressMB]+0 in :0", + "void java.util.Random.nextBytes(byte[])+3 in Random.java:472", + "byte[] ShaShenanigans.hashRandomStuff()+3 in ShaShenanigans.java:29", + "void ShaShenanigans.shaShenanigans()+2 in ShaShenanigans.java:20", + "void ShaShenanigans.main(java.lang.String[])+0 in ShaShenanigans.java:13", + "StubRoutines (initial stubs) [call_stub_return_address]+0 in :0", + "libjvm.so+0x833107", + "libjvm.so+0x8cc613", + "libjvm.so+0x8ce2cb", + "libjli.so+0x4b17", + "libjli.so+0x737b", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121746, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x806d7", + "libjli.so+0x7ebf", + "libjli.so+0x54a3", + "libjli.so+0x65bf", + "java+0xacb", + "libc.so.6+0x2777f", + "libc.so.6+0x27857", + "java+0xb6f" + ] + }, + { + "lwp": 121749, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x8732b", + "libjvm.so+0xc61a4b", + "libjvm.so+0xe6e417", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121750, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xbbf393", + "libjvm.so+0xb6d8b7", + "libjvm.so+0x70aef7", + "libjvm.so+0x5e195b", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121751, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x8732b", + "libjvm.so+0xc61a4b", + "libjvm.so+0xe6e417", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121752, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xbbf393", + "libjvm.so+0xb6d8b7", + "libjvm.so+0x711da7", + "libjvm.so+0x7121df", + "libjvm.so+0x5e195b", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121753, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xbbf2ef", + "libjvm.so+0xb6d8b7", + "libjvm.so+0x76947b", + "libjvm.so+0x76982f", + "libjvm.so+0x5e195b", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121754, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xbbf2ef", + "libjvm.so+0xb6d8b7", + "libjvm.so+0xe57613", + "libjvm.so+0xe5816b", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121755, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xbbf393", + "libjvm.so+0xb6d943", + "libjvm.so+0x8f2ff7", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:246", + "void java.lang.ref.Reference$ReferenceHandler.run()+3 in Reference.java:208", + "StubRoutines (initial stubs) [call_stub_return_address]+0 in :0", + "libjvm.so+0x833107", + "libjvm.so+0x8345cb", + "libjvm.so+0x8f0c5b", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121756, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xbbea13", + "libjvm.so+0xb91b17", + "libjvm.so+0xd98f2f", + "libjvm.so+0x8f49a3", + "void java.lang.Object.wait0(long)+0 in Object.java:0", + "void java.lang.Object.wait(long)+2 in Object.java:366", + "void java.lang.Object.wait()+0 in Object.java:339", + "void java.lang.ref.NativeReferenceQueue.await()+0 in NativeReferenceQueue.java:48", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove0()+2 in ReferenceQueue.java:158", + "java.lang.ref.Reference java.lang.ref.NativeReferenceQueue.remove()+1 in NativeReferenceQueue.java:89", + "void java.lang.ref.Finalizer$FinalizerThread.run()+7 in Finalizer.java:173", + "StubRoutines (initial stubs) [call_stub_return_address]+0 in :0", + "libjvm.so+0x833107", + "libjvm.so+0x8345cb", + "libjvm.so+0x8f0c5b", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121757, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x8732b", + "libjvm.so+0xc61a4b", + "libjvm.so+0xd0fb2b", + "libjvm.so+0xba8b63", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121758, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xbbf393", + "libjvm.so+0xb6d8b7", + "libjvm.so+0xc64313", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121759, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xbbf2ef", + "libjvm.so+0xb6d8b7", + "libjvm.so+0xb60beb", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121760, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xbbf2ef", + "libjvm.so+0xb6d943", + "libjvm.so+0x5c5a9f", + "libjvm.so+0x5c910b", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121761, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xbbf2ef", + "libjvm.so+0xb6d943", + "libjvm.so+0x5c5a9f", + "libjvm.so+0x5c910b", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121762, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e18f", + "libjvm.so+0xbbf393", + "libjvm.so+0xb6d8b7", + "libjvm.so+0xb8564f", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121763, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xbbf2ef", + "libjvm.so+0xb6d8b7", + "libjvm.so+0xb8519b", + "libjvm.so+0xb8525f", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + }, + { + "lwp": 121764, + "frames": [ + "libc.so.6+0x7b654", + "libc.so.6+0x7e47f", + "libjvm.so+0xbbef0f", + "libjvm.so+0xe00a2b", + "void jdk.internal.misc.Unsafe.park(boolean, long)+0 in Unsafe.java:0", + "void java.util.concurrent.locks.LockSupport.parkNanos(java.lang.Object, long)+7 in LockSupport.java:269", + "boolean java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(long, java.util.concurrent.TimeUnit)+16 in AbstractQueuedSynchronizer.java:1847", + "void java.lang.ref.ReferenceQueue.await(long)+0 in ReferenceQueue.java:71", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove0(long)+4 in ReferenceQueue.java:143", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+7 in ReferenceQueue.java:218", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.runWith(java.lang.Object, java.lang.Runnable)+1 in Thread.java:1596", + "void java.lang.Thread.run()+3 in Thread.java:1583", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:186", + "StubRoutines (initial stubs) [call_stub_return_address]+0 in :0", + "libjvm.so+0x833107", + "libjvm.so+0x8345cb", + "libjvm.so+0x8f0c5b", + "libjvm.so+0x8490f3", + "libjvm.so+0xdcbbc7", + "libjvm.so+0xbb53ab", + "libc.so.6+0x7edd7", + "libc.so.6+0xe7e5b" + ] + } + ], + "modules": [ + { + "ref": "ae6bd25b1f9616e37fb652d1052af984576d22adacfd3bced9df82d075ad92c7", + "local-path": "/usr/lib/aarch64-linux-gnu/libm.so.6" + }, + { + "ref": "3d9ec598f8e4081d73da9ec3b602201f668caf7d309bff6fb32c10ad3fb3c1c0", + "local-path": "/usr/lib/jvm/java-21-openjdk-arm64/lib/server/libjvm.so" + }, + { + "ref": "ffb1ab496e6eced03ab679075f9f2c415c7728a145cc7f63d614497102d73822", + "local-path": "/usr/lib/aarch64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "44bbd3b7b29bc29454e8350f6346b36798e371702fd8bb74c1055cb3e680e48e", + "local-path": "/usr/lib/jvm/java-21-openjdk-arm64/lib/libjli.so" + }, + { + "ref": "fd9fc7af90a3817a1e01a35ed92500e2b80a046b495a3418c469aa0d7a77f428", + "local-path": "/usr/lib/jvm/java-21-openjdk-arm64/bin/java" + }, + { + "ref": "bb65f57adff3fd6f2c1f63c640873f2dec8ec3781bec383231d9b65930ed629d", + "local-path": "/usr/lib/jvm/java-21-openjdk-arm64/lib/libjava.so" + }, + { + "ref": "53b3b3700ebbd727460a841e6f1d370a66dae7f7c3efee32222dcc82593e337a", + "local-path": "/usr/lib/jvm/java-21-openjdk-arm64/lib/libjimage.so" + }, + { + "ref": "72eb811bb6d61300f62f9e9e4d824759d3c6a8f510d157999d8d2adb6dc683d8", + "local-path": "/usr/lib/aarch64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "ff608307ebc72d45b92578b3eaaa0430b133c4a6625a2ed4088f8ea921973b4f", + "local-path": "/usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.32" + }, + { + "ref": "02393b2bdc6555140149dda7b17c94d1c6532d343fe42f7a06eb090c9c8f51ed", + "local-path": "/usr/lib/jvm/java-21-openjdk-arm64/lib/libzip.so" + }, + { + "ref": "ae89390ee9874d9b0c4f7eda0ba8c798d631df0e600549d6a15f7237f03c0317", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + }, + { + "ref": "388929059d814ffeedda0beaff53722c24c59838817324d95221c18ed851f21c", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json b/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json new file mode 100644 index 00000000..f74cda93 --- /dev/null +++ b/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json @@ -0,0 +1,295 @@ +{ + "coredump-ref": "c901e34f8c64796d785b8e5ef0a2cec545426ed66a91b396df46721ebeb695a5", + "threads": [ + { + "lwp": 377038, + "frames": [ + "int PrologueEpilogue.b()+0 in PrologueEpilogue.java:17", + "int PrologueEpilogue.a()+0 in PrologueEpilogue.java:13", + "void PrologueEpilogue.main(java.lang.String[])+2 in PrologueEpilogue.java:27", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x86368b", + "libjvm.so+0x865b6f", + "libjli.so+0x40cb", + "libjli.so+0x723b", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377045, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20b87", + "libjvm.so+0x894b8b", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377049, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb14543", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377026, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x82a7", + "libjli.so+0x7d8b", + "libjli.so+0x5293", + "libjli.so+0x66ff", + "java+0xacf", + "libc-2.33.so+0x21613", + "java+0xb77" + ] + }, + { + "lwp": 377050, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20b87", + "libjvm.so+0x59963b", + "libjvm.so+0x59bdfb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377047, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xcf4303", + "libjvm.so+0xb569d3", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377044, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xe3ed57", + "libjvm.so+0xe3f93b", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377039, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 377040, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0x6ad5d7", + "libjvm.so+0x6ae643", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377041, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 377042, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0x6afe5f", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377053, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb353fb", + "libjvm.so+0xb354e7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377043, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0x706fd7", + "libjvm.so+0x7071f3", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377054, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6d75f", + "libjvm.so+0xb413b3", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377052, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb35973", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377051, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xd74027", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377046, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6d573", + "libjvm.so+0xb41453", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377048, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xc0a5bb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ldp-x29-x30.376761.json b/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ldp-x29-x30.376761.json new file mode 100644 index 00000000..638dd755 --- /dev/null +++ b/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ldp-x29-x30.376761.json @@ -0,0 +1,295 @@ +{ + "coredump-ref": "1a7cfa26da18c8aee8848820935dddf18e37f5bfde323003e779f11eee4f0964", + "threads": [ + { + "lwp": 376770, + "frames": [ + "int PrologueEpilogue.b()+0 in PrologueEpilogue.java:17", + "int PrologueEpilogue.a()+0 in PrologueEpilogue.java:13", + "void PrologueEpilogue.main(java.lang.String[])+2 in PrologueEpilogue.java:27", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x86368b", + "libjvm.so+0x865b6f", + "libjli.so+0x40cb", + "libjli.so+0x723b", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376775, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0x706fd7", + "libjvm.so+0x7071f3", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376774, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0x6afe5f", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376773, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 376772, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0x6ad5d7", + "libjvm.so+0x6ae643", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376777, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20b87", + "libjvm.so+0x894b8b", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376778, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6d573", + "libjvm.so+0xb41453", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376779, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xcf4303", + "libjvm.so+0xb569d3", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376782, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20b87", + "libjvm.so+0x59963b", + "libjvm.so+0x59bdfb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376776, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xe3ed57", + "libjvm.so+0xe3f93b", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376784, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb35973", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376781, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb14543", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376780, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xc0a5bb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376761, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x82a7", + "libjli.so+0x7d8b", + "libjli.so+0x5293", + "libjli.so+0x66ff", + "java+0xacf", + "libc-2.33.so+0x21613", + "java+0xb77" + ] + }, + { + "lwp": 376785, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb353fb", + "libjvm.so+0xb354e7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376783, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xd74027", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 376771, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 376786, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6d75f", + "libjvm.so+0xb413b3", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ret.377192.json b/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ret.377192.json new file mode 100644 index 00000000..2ed5dbbd --- /dev/null +++ b/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ret.377192.json @@ -0,0 +1,295 @@ +{ + "coredump-ref": "d791eb1043b3a6f0ac62465e757912de302a16a09332661eebbd1084db2635a1", + "threads": [ + { + "lwp": 377204, + "frames": [ + "int PrologueEpilogue.b()+0 in PrologueEpilogue.java:17", + "int PrologueEpilogue.a()+0 in PrologueEpilogue.java:13", + "void PrologueEpilogue.main(java.lang.String[])+2 in PrologueEpilogue.java:27", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x86368b", + "libjvm.so+0x865b6f", + "libjli.so+0x40cb", + "libjli.so+0x723b", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377211, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20b87", + "libjvm.so+0x894b8b", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377215, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb14543", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377207, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 377212, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6d573", + "libjvm.so+0xb41453", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377206, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0x6ad5d7", + "libjvm.so+0x6ae643", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377217, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xd74027", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377213, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xcf4303", + "libjvm.so+0xb569d3", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377208, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0x6afe5f", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377209, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0x706fd7", + "libjvm.so+0x7071f3", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377205, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 377192, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x82a7", + "libjli.so+0x7d8b", + "libjli.so+0x5293", + "libjli.so+0x66ff", + "java+0xacf", + "libc-2.33.so+0x21613", + "java+0xb77" + ] + }, + { + "lwp": 377216, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20b87", + "libjvm.so+0x59963b", + "libjvm.so+0x59bdfb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377210, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xe3ed57", + "libjvm.so+0xe3f93b", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377219, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb353fb", + "libjvm.so+0xb354e7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377218, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb35973", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377214, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xc0a5bb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 377220, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6d75f", + "libjvm.so+0xb413b3", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.stp-fp-lr.json b/utils/coredump/testdata/arm64/java.PrologueEpilogue.stp-fp-lr.json new file mode 100644 index 00000000..eedcad39 --- /dev/null +++ b/utils/coredump/testdata/arm64/java.PrologueEpilogue.stp-fp-lr.json @@ -0,0 +1,296 @@ +{ + "coredump-ref": "eb1c60ffc781e74aa1d0c9b85b5a8faa7578e4b05539705a3c8241a194c5c003", + "threads": [ + { + "lwp": 85932, + "frames": [ + "int PrologueEpilogue.c()+0 in PrologueEpilogue.java:21", + "int PrologueEpilogue.b()+0 in PrologueEpilogue.java:17", + "int PrologueEpilogue.a()+0 in PrologueEpilogue.java:13", + "void PrologueEpilogue.main(java.lang.String[])+2 in PrologueEpilogue.java:27", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x86368b", + "libjvm.so+0x865b6f", + "libjli.so+0x40cb", + "libjli.so+0x723b", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85938, + "frames": [ + "libpthread-2.33.so+0x143c8", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xe3ed57", + "libjvm.so+0xe3f93b", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85939, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20b87", + "libjvm.so+0x894b8b", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85935, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 85941, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xcf4303", + "libjvm.so+0xb569d3", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85936, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0x6afe5f", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85920, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x82a7", + "libjli.so+0x7d8b", + "libjli.so+0x5293", + "libjli.so+0x66ff", + "java+0xacf", + "libc-2.33.so+0x21613", + "java+0xb77" + ] + }, + { + "lwp": 85933, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 85934, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0x6ad5d7", + "libjvm.so+0x6ae643", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85942, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xc0a5bb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85945, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xd74027", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85937, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0x706fd7", + "libjvm.so+0x7071f3", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85944, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20b87", + "libjvm.so+0x59963b", + "libjvm.so+0x59bdfb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85946, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb35973", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85943, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb14543", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85940, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6d573", + "libjvm.so+0xb41453", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85948, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6d75f", + "libjvm.so+0xb413b3", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85947, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb353fb", + "libjvm.so+0xb354e7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.sub-sp-sp.json b/utils/coredump/testdata/arm64/java.PrologueEpilogue.sub-sp-sp.json new file mode 100644 index 00000000..493d6790 --- /dev/null +++ b/utils/coredump/testdata/arm64/java.PrologueEpilogue.sub-sp-sp.json @@ -0,0 +1,296 @@ +{ + "coredump-ref": "16a26b9e217636f42dd3e974119c2f0c50e046cc5d66bc422dc7bd113f69d830", + "threads": [ + { + "lwp": 85689, + "frames": [ + "int PrologueEpilogue.c()+0 in PrologueEpilogue.java:21", + "int PrologueEpilogue.b()+0 in PrologueEpilogue.java:17", + "int PrologueEpilogue.a()+0 in PrologueEpilogue.java:13", + "void PrologueEpilogue.main(java.lang.String[])+2 in PrologueEpilogue.java:27", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x86368b", + "libjvm.so+0x865b6f", + "libjli.so+0x40cb", + "libjli.so+0x723b", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85690, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 85693, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0x6afe5f", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85698, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xcf4303", + "libjvm.so+0xb569d3", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85695, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xe3ed57", + "libjvm.so+0xe3f93b", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85697, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6d573", + "libjvm.so+0xb41453", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85694, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0x706fd7", + "libjvm.so+0x7071f3", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85696, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20b87", + "libjvm.so+0x894b8b", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85677, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x82a7", + "libjli.so+0x7d8b", + "libjli.so+0x5293", + "libjli.so+0x66ff", + "java+0xacf", + "libc-2.33.so+0x21613", + "java+0xb77" + ] + }, + { + "lwp": 85692, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 85699, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xc0a5bb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85691, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0x6ad5d7", + "libjvm.so+0x6ae643", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85702, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xd74027", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85700, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb14543", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85701, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20b87", + "libjvm.so+0x59963b", + "libjvm.so+0x59bdfb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85704, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb353fb", + "libjvm.so+0xb354e7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85705, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6d75f", + "libjvm.so+0xb413b3", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 85703, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb35973", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/java.VdsoPressure.osJavaTimeNanos.649996.json b/utils/coredump/testdata/arm64/java.VdsoPressure.osJavaTimeNanos.649996.json new file mode 100644 index 00000000..9fd1d2fd --- /dev/null +++ b/utils/coredump/testdata/arm64/java.VdsoPressure.osJavaTimeNanos.649996.json @@ -0,0 +1,310 @@ +{ + "coredump-ref": "556a135e412459affae1dbfc9cf63349e65fc648bda73a46a655c7ce105f133a", + "threads": [ + { + "lwp": 650005, + "frames": [ + "libjvm.so+0xb6d45c", + "void VdsoPressure.main(java.lang.String[])+0 in VdsoPressure.java:5", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x86368b", + "libjvm.so+0x865b6f", + "libjli.so+0x40cb", + "libjli.so+0x723b", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650007, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0x6ad5d7", + "libjvm.so+0x6ae643", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650009, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0x6afe5f", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650015, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xc0a5bb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650020, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb35973", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 649996, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x82a7", + "libjli.so+0x7d8b", + "libjli.so+0x5293", + "libjli.so+0x66ff", + "java+0xacf", + "libc-2.33.so+0x21613", + "java+0xb77" + ] + }, + { + "lwp": 650011, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xe3ed57", + "libjvm.so+0xe3f93b", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650016, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb14543", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650006, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 650013, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6d573", + "libjvm.so+0xb41453", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove()+0 in ReferenceQueue.java:176", + "void java.lang.ref.Finalizer$FinalizerThread.run()+17 in Finalizer.java:172", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650014, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xcf4303", + "libjvm.so+0xb569d3", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650018, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20b87", + "libjvm.so+0x59963b", + "libjvm.so+0x59bdfb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650019, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xd74027", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650021, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0xb353fb", + "libjvm.so+0xb354e7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650012, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd547", + "libjvm.so+0xb6df57", + "libjvm.so+0xb20b87", + "libjvm.so+0x894b8b", + "void java.lang.ref.Reference.waitForReferencePendingList()+0 in Reference.java:0", + "void java.lang.ref.Reference.processPendingReferences()+0 in Reference.java:253", + "void java.lang.ref.Reference$ReferenceHandler.run()+0 in Reference.java:215", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650022, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6d75f", + "libjvm.so+0xb413b3", + "libjvm.so+0xd7dc2b", + "libjvm.so+0x88d7b7", + "void java.lang.Object.wait(long)+0 in Object.java:0", + "java.lang.ref.Reference java.lang.ref.ReferenceQueue.remove(long)+8 in ReferenceQueue.java:155", + "void jdk.internal.ref.CleanerImpl.run()+12 in CleanerImpl.java:140", + "void java.lang.Thread.run()+1 in Thread.java:833", + "void jdk.internal.misc.InnocuousThread.run()+2 in InnocuousThread.java:162", + "StubRoutines (1) [call_stub_return_address]+0 in :0", + "libjvm.so+0x7d1f3b", + "libjvm.so+0x7d3233", + "libjvm.so+0x88c65f", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650010, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20ae7", + "libjvm.so+0x706fd7", + "libjvm.so+0x7071f3", + "libjvm.so+0x5b1f97", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + }, + { + "lwp": 650008, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0x101fb", + "libjvm.so+0xc09c1b", + "libjvm.so+0xe597f7", + "libjvm.so+0xe5988f", + "" + ] + }, + { + "lwp": 650017, + "frames": [ + "libpthread-2.33.so+0x143c4", + "libpthread-2.33.so+0xd85b", + "libjvm.so+0xb6df03", + "libjvm.so+0xb20b87", + "libjvm.so+0x59963b", + "libjvm.so+0x59bdfb", + "libjvm.so+0xdc07f7", + "libjvm.so+0xdc5633", + "libjvm.so+0xb63fbb", + "libpthread-2.33.so+0x6f3b", + "libc-2.33.so+0xd2cdb" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/perl-5.38-debian.json b/utils/coredump/testdata/arm64/perl-5.38-debian.json new file mode 100644 index 00000000..a6da0791 --- /dev/null +++ b/utils/coredump/testdata/arm64/perl-5.38-debian.json @@ -0,0 +1,41 @@ +{ + "coredump-ref": "6c3938b0a7f50a81f329046290b83d95f28924691d3530608ce6bb36d8ec1b76", + "threads": [ + { + "lwp": 1212873, + "frames": [ + "perl+0x1228d0", + "perl+0x13426f", + "+0 in hi.pl:19", + "perl+0x128563", + "perl+0x6d5d7", + "perl+0x44907", + "libc.so.6+0x26dc3", + "libc.so.6+0x26e97", + "perl+0x4496f" + ] + } + ], + "modules": [ + { + "ref": "93e7f5b5025839d2f8044bcc3fff187da48a5bd72d009d82fcf291f9eeafec36", + "local-path": "/usr/bin/perl" + }, + { + "ref": "7c3e45de45fa8de1ddc924678f9dfa4342ae0addb23c8491b3997053dfbe2524", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + }, + { + "ref": "c262feb3322a8a30259ba80fd68302394aaee6a46614dad7be2dcbf4d27f4cf7", + "local-path": "/usr/lib/aarch64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "6fe4505b7f395f8a104ae2e3eb1f130fc585037922d8b61607e2728c6ab3419f", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + }, + { + "ref": "d03f73f8005b96a248a2e8f4a9fb2d5b15050303cd56fed1d64e9f50db6713a7", + "local-path": "/usr/lib/aarch64-linux-gnu/libm.so.6" + } + ] +} diff --git a/utils/coredump/testdata/arm64/php-8.2.10-prime.json b/utils/coredump/testdata/arm64/php-8.2.10-prime.json new file mode 100644 index 00000000..00dd558b --- /dev/null +++ b/utils/coredump/testdata/arm64/php-8.2.10-prime.json @@ -0,0 +1,196 @@ +{ + "coredump-ref": "727f285782be05983d61d29e7635b1e633622ae36c45cd2faf87cf694d964839", + "threads": [ + { + "lwp": 90223, + "frames": [ + "php8.2+0x332458", + "is_prime+13 in /pwd/testsources/php/prime.php:16", + "+33 in /pwd/testsources/php/prime.php:34", + "php8.2+0x36b69b", + "php8.2+0x37744f", + "php8.2+0x2efa43", + "php8.2+0x28501f", + "php8.2+0x3ec38f", + "php8.2+0x10d8eb", + "libc.so.6+0x26dbf", + "libc.so.6+0x26e97", + "php8.2+0x10d9ef" + ] + } + ], + "modules": [ + { + "ref": "c1499fc461c990a316b373e093f92ae8a3a86662870d79a5569516911336978e", + "local-path": "/usr/bin/php8.2" + }, + { + "ref": "27a9c9c303d1d7c96784f0633f2b372309d5e1bd9c287ea1a97919ebdc8fc5db", + "local-path": "/usr/lib/php/20220829/tokenizer.so" + }, + { + "ref": "85a568a031ec808b29cf64af2ed610261bf768f2f88e9e91b929111ba3aed970", + "local-path": "/usr/lib/php/20220829/sysvshm.so" + }, + { + "ref": "2fc82883fb50da1286865f24c3fd12088f6513e1b3db22e45b39786d18a444f6", + "local-path": "/usr/lib/php/20220829/sysvsem.so" + }, + { + "ref": "2f28aedc7e40130ffbfbd7cd198e0b0c1f0a7cc54b9e6e58ae7a5ffeeb2e04be", + "local-path": "/usr/lib/php/20220829/sysvmsg.so" + }, + { + "ref": "ff1c9de89f3c7fbce8426659e44c357a3e82ae894c781ddb8c2031f995c36697", + "local-path": "/usr/lib/php/20220829/sockets.so" + }, + { + "ref": "7e959c4a47439fc134268206802e2c203eea1ded1ebff31d2c39a4911ac2b788", + "local-path": "/usr/lib/php/20220829/shmop.so" + }, + { + "ref": "fa4695c07b14a261a965a54bedaa804e4993135ed8224e6803a887a6b06aba06", + "local-path": "/usr/lib/aarch64-linux-gnu/libmd.so.0.1.0" + }, + { + "ref": "9cf5efeed26260b1e4aee677bd835622b8aee97994764a0e8fa3db48b0fefef1", + "local-path": "/usr/lib/aarch64-linux-gnu/libbsd.so.0.11.7" + }, + { + "ref": "23f05cbfa95a9e2810c5130f23465b451001b77ef478b883d992bfb8864c4b3f", + "local-path": "/usr/lib/aarch64-linux-gnu/libtinfo.so.6.4" + }, + { + "ref": "36a8a4e09f4ef6b4be74999b0363d559d42b0b21254d77a1dcff4293a2031e04", + "local-path": "/usr/lib/aarch64-linux-gnu/libedit.so.2.0.72" + }, + { + "ref": "9028fc6bc4d1972cee09c166abf75371c2bbcfa7f8fddc3f7b9112b58f3d1367", + "local-path": "/usr/lib/php/20220829/readline.so" + }, + { + "ref": "ef1d0d647c8dce2c893572d303ca4a5fc1f8cd40b88efe08cdbfa3ce3f980828", + "local-path": "/usr/lib/php/20220829/posix.so" + }, + { + "ref": "f7886e8cbfe1a55bfad98bd2a5e09f83fb5bf4454882b10bb9a0945d2125be12", + "local-path": "/usr/lib/php/20220829/phar.so" + }, + { + "ref": "03711554515dbe921b426bedc2e741f073df4c8e332b73039de46b688ddd942f", + "local-path": "/usr/lib/php/20220829/iconv.so" + }, + { + "ref": "d01dde093397096a9e548c06726956dd1744ae0a6e62e5e69013744cbe8e818b", + "local-path": "/usr/lib/php/20220829/gettext.so" + }, + { + "ref": "c12650bdaf40d3061ed62c6ecde47a7d23dece099e42641efd514e5054735cbc", + "local-path": "/usr/lib/php/20220829/ftp.so" + }, + { + "ref": "7d4f3195f44fcd4429bd314432ad251f4880188ba317f7d50a535cbc0256db84", + "local-path": "/usr/lib/php/20220829/fileinfo.so" + }, + { + "ref": "cbae420bdec2903fa5943664504b3540091651ac0af76403741a8c14d5b018da", + "local-path": "/usr/lib/aarch64-linux-gnu/libffi.so.8.1.2" + }, + { + "ref": "a831ecdc2e4342078297420af89ad45e34d05253f8dcc2cd287435901927b53f", + "local-path": "/usr/lib/php/20220829/ffi.so" + }, + { + "ref": "798f43360d956af67f4b7d8076726e49a7b589227bf7796de04d47095b1917d2", + "local-path": "/usr/lib/php/20220829/exif.so" + }, + { + "ref": "8f0780a623251b40123e4e87f7ea1d2d165376a9c2cbe17cc7da836c331ea78d", + "local-path": "/usr/lib/php/20220829/ctype.so" + }, + { + "ref": "4227944abf5c4de78d889394c9fc553629593fa363b86fb986f7b431a49a53f2", + "local-path": "/usr/lib/php/20220829/calendar.so" + }, + { + "ref": "996ae25b5c2b5adb1e6695ace609debd2c0893b772b24b365c51231c4f94d277", + "local-path": "/usr/lib/php/20220829/pdo.so" + }, + { + "ref": "c26ff01da7273a3c41b4369d4603d355b2be369173461d80ec358328130dca5d", + "local-path": "/usr/lib/php/20220829/opcache.so" + }, + { + "ref": "4af23bb40c8f2e80a26c95369b442986213c50a7308d8d73b85c4911dde0a358", + "local-path": "/usr/lib/locale/C.utf8/LC_CTYPE" + }, + { + "ref": "25824787b4472ff4e6f6351f11d3404d38bcf21c88895148e846a067d7e2a4a1", + "local-path": "/usr/lib/aarch64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "c667f83e7859d9739d5e6a57d5f0e59fafe9783ea0ec0817a8d3f6729ce3c780", + "local-path": "/usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.32" + }, + { + "ref": "fc2600e8e114e772845b9a061fa0d6fd6f8d73b0f213d70e88763127b4d7443a", + "local-path": "/usr/lib/aarch64-linux-gnu/libicudata.so.72.1" + }, + { + "ref": "c349203abb8940c705b574a2bf7ddfa95ddb38a1747f0432a55bbe15c04354c5", + "local-path": "/usr/lib/aarch64-linux-gnu/libpthread.so.0" + }, + { + "ref": "71498130dc7db99fb4aae918296810aec31027d25515d9c305334e0703af7ba9", + "local-path": "/usr/lib/aarch64-linux-gnu/liblzma.so.5.4.4" + }, + { + "ref": "14fc1d32a6c09f0ae248b361dcfb7def2b6501b337612fd0c6200fc47d2de59c", + "local-path": "/usr/lib/aarch64-linux-gnu/libicuuc.so.72.1" + }, + { + "ref": "a0b2f1cbd2b8bc1efac372d853617c94c0a10f81b4252cbafe2d9f0f98c898c0", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + }, + { + "ref": "aaa8f7771580f84c56c4a842bf27b7019d9910c13899050b5e801b1a1783375a", + "local-path": "/usr/lib/aarch64-linux-gnu/libargon2.so.1" + }, + { + "ref": "6616716f538e552f38664d55b3140d4c520a8410510561685649326fd9691445", + "local-path": "/usr/lib/aarch64-linux-gnu/libsodium.so.23.3.0" + }, + { + "ref": "5f3b195e9e56f9c212490d7a064a52ddb886ab9399fd87da8f975689747658d4", + "local-path": "/usr/lib/aarch64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "b534ab5dfc92672a585dd998f1880c4e8fa3a107435546b8327d70e31af5e9e6", + "local-path": "/usr/lib/aarch64-linux-gnu/libpcre2-8.so.0.11.2" + }, + { + "ref": "d15a8500f1ae592f588c017c039cd54661899aeab1a5daed60c295828b90e45c", + "local-path": "/usr/lib/aarch64-linux-gnu/libcrypto.so.3" + }, + { + "ref": "72ce13a229f5e206da6e99a2f60ebb8f17b99f497c9d106c89adeab582bd8ec2", + "local-path": "/usr/lib/aarch64-linux-gnu/libssl.so.3" + }, + { + "ref": "1b5a5b39c2702435bfaea0ac47ac9611b98096e90e8afb6a0a739ad68b5106db", + "local-path": "/usr/lib/aarch64-linux-gnu/libxml2.so.2.9.14" + }, + { + "ref": "c4855f733f0504c8efcb813dc1223c57a2945cea9fe0488aa3ff823a26af484e", + "local-path": "/usr/lib/aarch64-linux-gnu/libm.so.6" + }, + { + "ref": "c4c52c0df6f3809f1b85927a85f322d102cffebb6b517b44b37048f6fd620c01", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "8204c57234aba6a74f5c3429969f896769bc6a12df679f8f22f318816cb53db4", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/php-8.2.7-prime.json b/utils/coredump/testdata/arm64/php-8.2.7-prime.json new file mode 100644 index 00000000..42c11cc0 --- /dev/null +++ b/utils/coredump/testdata/arm64/php-8.2.7-prime.json @@ -0,0 +1,195 @@ +{ + "coredump-ref": "ea9d8330742e987b0d598d804d66555513f947dac4be8a7c5480cc4333149a6a", + "threads": [ + { + "lwp": 79250, + "frames": [ + "is_prime+13 in /pwd/testsources/php/prime.php:16", + "+33 in /pwd/testsources/php/prime.php:34", + "php8.2+0x3637af", + "php8.2+0x36b7cb", + "php8.2+0x2e7c13", + "php8.2+0x28156f", + "php8.2+0x3ddc6f", + "php8.2+0x10d73f", + "libc.so.6+0x2777f", + "libc.so.6+0x27857", + "php8.2+0x10d82f" + ] + } + ], + "modules": [ + { + "ref": "722122d0fb59ebdcd18ba0846f5e372210768ff366e7631f2f907a0396be5c29", + "local-path": "/usr/bin/php8.2" + }, + { + "ref": "6a518aa98364e014022ff5434f161befaccfac0075bfcc3b6f6989f89adeeacb", + "local-path": "/usr/lib/php/20220829/tokenizer.so" + }, + { + "ref": "875d460950fdc96e52a05b1150f0c1c7b3202dedcc8889cafd517b0b1e9f5a8a", + "local-path": "/usr/lib/php/20220829/sysvshm.so" + }, + { + "ref": "b6617eec506ec3385a32a1259fc90135dad7a72939e73c10ec931dc985e524fa", + "local-path": "/usr/lib/php/20220829/sysvsem.so" + }, + { + "ref": "022837ff617038e2835fcdd4cb3e28bdbd2923ea1a0ebc39311d4f68e9ff6d4c", + "local-path": "/usr/lib/php/20220829/sysvmsg.so" + }, + { + "ref": "60954293ac6efe9c2dfdd460eab49ef5c6e7193557712ba53e3f526783774310", + "local-path": "/usr/lib/php/20220829/sockets.so" + }, + { + "ref": "5a4b5865323ab5321681b048d59836c1aacebc88cf71d2a65b1d3b13586b108c", + "local-path": "/usr/lib/php/20220829/shmop.so" + }, + { + "ref": "be8d151f2f06935db812859f575ec818af7fd93eaab0ec4e7dae9e5ad7ca38bb", + "local-path": "/usr/lib/aarch64-linux-gnu/libmd.so.0.0.5" + }, + { + "ref": "24f2bb08fed31a04a80f4d0d0145f4cc5f42543af07a364b1fafdd3d2dcbe137", + "local-path": "/usr/lib/aarch64-linux-gnu/libbsd.so.0.11.7" + }, + { + "ref": "3486c8bf9712132d1834a4800d5002e37d4edc670daa9652b3d470eb813aec55", + "local-path": "/usr/lib/aarch64-linux-gnu/libtinfo.so.6.4" + }, + { + "ref": "28f84ddeeab3945456e400c929f0393a9f9a6ddf88a0cdfe45062008fc3f1334", + "local-path": "/usr/lib/aarch64-linux-gnu/libedit.so.2.0.70" + }, + { + "ref": "991c4be866f4c4ed3cff8b0b40fc9bbb31f02c7ca1d20b3f8b6f249b1c729651", + "local-path": "/usr/lib/php/20220829/readline.so" + }, + { + "ref": "6b9a106b6bc3f8ff41910ae70e04e453893275974ee632298a33eb36a01d07c5", + "local-path": "/usr/lib/php/20220829/posix.so" + }, + { + "ref": "791386992d052dfd9e46a91abd7b07106e964e3881d133c0dc6a91536141f5a1", + "local-path": "/usr/lib/php/20220829/phar.so" + }, + { + "ref": "30d3e037519516e87fcf5285d04aec0135d0de89ae263e9c3c3763b214f9d518", + "local-path": "/usr/lib/php/20220829/iconv.so" + }, + { + "ref": "7ed2a8efa43166c9262b766f375a0d0e41dbe4ed48dc3e9be6286b70085b267f", + "local-path": "/usr/lib/php/20220829/gettext.so" + }, + { + "ref": "07a248b510fd369d1ef6bc08992bce625895b19abd8f3bb52798ff69337432ef", + "local-path": "/usr/lib/php/20220829/ftp.so" + }, + { + "ref": "37bd546e8f02b1455e98641a006fb6052d440767bea807c346e9421922dd1e85", + "local-path": "/usr/lib/php/20220829/fileinfo.so" + }, + { + "ref": "cbae420bdec2903fa5943664504b3540091651ac0af76403741a8c14d5b018da", + "local-path": "/usr/lib/aarch64-linux-gnu/libffi.so.8.1.2" + }, + { + "ref": "ddac171bf5203bb858d98d2057065f5e1ed72db3de7652f6b908422083396539", + "local-path": "/usr/lib/php/20220829/ffi.so" + }, + { + "ref": "04b3953ffc4078d38dde05fe1ebb01ba6b2875ec597bf4e0eb59e7a15afa96d8", + "local-path": "/usr/lib/php/20220829/exif.so" + }, + { + "ref": "0b14041a284827d736e54e88fceb0541e36b824878780aa93e78eb1e0327b1e5", + "local-path": "/usr/lib/php/20220829/ctype.so" + }, + { + "ref": "269b7625abed1259bd9646534ad02aa4d22c68921f3a9160e47e3faa830dbe72", + "local-path": "/usr/lib/php/20220829/calendar.so" + }, + { + "ref": "bcccecac77c5a2cb8c87fda0e09f84ad3aa1a2d1e6c1138205224960c9879d25", + "local-path": "/usr/lib/php/20220829/pdo.so" + }, + { + "ref": "8399fa3cbae6817f14a03211698df3111baa546f18224476e88c67568c7e4f94", + "local-path": "/usr/lib/php/20220829/opcache.so" + }, + { + "ref": "e4b5576b19e40be5923b0eb864750d35944404bb0a92aa68d1a9b96110c52120", + "local-path": "/usr/lib/locale/C.utf8/LC_CTYPE" + }, + { + "ref": "8526cf244c909542d584aa8454212341290a4537020b9bd30c7f2d3f7c1bc308", + "local-path": "/usr/lib/aarch64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "8e62ed4579918c515c4f0d939d7bbe44048e501021783db840220deb5bb982e0", + "local-path": "/usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.30" + }, + { + "ref": "fc2600e8e114e772845b9a061fa0d6fd6f8d73b0f213d70e88763127b4d7443a", + "local-path": "/usr/lib/aarch64-linux-gnu/libicudata.so.72.1" + }, + { + "ref": "a40ef89105f57053526e0347921184cf954c6986613e4121c4a46fd8c5bf29d3", + "local-path": "/usr/lib/aarch64-linux-gnu/libpthread.so.0" + }, + { + "ref": "89c4407fd4022312b12b2144aed975f6015aa3e51844cdf34ff1e1f8dd05576a", + "local-path": "/usr/lib/aarch64-linux-gnu/liblzma.so.5.4.1" + }, + { + "ref": "14fc1d32a6c09f0ae248b361dcfb7def2b6501b337612fd0c6200fc47d2de59c", + "local-path": "/usr/lib/aarch64-linux-gnu/libicuuc.so.72.1" + }, + { + "ref": "e8ccdb5ffa5e29c0692e5afcb4e1f3f536067bb261e79e8a089a163d0fafdd45", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + }, + { + "ref": "064169773963575e6d5baf729279064bf1ae0c36256f52d2d514b970ec14af57", + "local-path": "/usr/lib/aarch64-linux-gnu/libargon2.so.1" + }, + { + "ref": "6616716f538e552f38664d55b3140d4c520a8410510561685649326fd9691445", + "local-path": "/usr/lib/aarch64-linux-gnu/libsodium.so.23.3.0" + }, + { + "ref": "ffb1ab496e6eced03ab679075f9f2c415c7728a145cc7f63d614497102d73822", + "local-path": "/usr/lib/aarch64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "cabf859fab34e77fba3ab3485878cc19014327246a7c6aa21ac0d2d1a32dcc81", + "local-path": "/usr/lib/aarch64-linux-gnu/libpcre2-8.so.0.11.2" + }, + { + "ref": "360a2464f1620bf95b86c608c58f116b63d100154d5825078bd4e5c5f0cd3530", + "local-path": "/usr/lib/aarch64-linux-gnu/libcrypto.so.3" + }, + { + "ref": "4f3d752767011448cc91a266501ca6ffaa319d908b3ae5d21010be8be38d73bc", + "local-path": "/usr/lib/aarch64-linux-gnu/libssl.so.3" + }, + { + "ref": "24a1d6da75c7267b38203c95845fcf85bdd2816c9bfe7667ca9557f3f351dcaa", + "local-path": "/usr/lib/aarch64-linux-gnu/libxml2.so.2.9.14" + }, + { + "ref": "ae6bd25b1f9616e37fb652d1052af984576d22adacfd3bced9df82d075ad92c7", + "local-path": "/usr/lib/aarch64-linux-gnu/libm.so.6" + }, + { + "ref": "c4c52c0df6f3809f1b85927a85f322d102cffebb6b517b44b37048f6fd620c01", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "5ece65ae57fce3774af5218e7c3de93f1cc79f70b92ae3bec86a4135fc2396c2", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/php81.571415.json b/utils/coredump/testdata/arm64/php81.571415.json new file mode 100644 index 00000000..68fa09b4 --- /dev/null +++ b/utils/coredump/testdata/arm64/php81.571415.json @@ -0,0 +1,23 @@ +{ + "coredump-ref": "98c8be16cd50150fcfba63823bcdf991dbfbb8b8577c4e722662d881170e82b6", + "threads": [ + { + "lwp": 571415, + "frames": [ + "php8.1+0x1ed574", + "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+0x349d77", + "php8.1+0x2cb883", + "php8.1+0x2666fb", + "php8.1+0x3bd2c3", + "php8.1+0x1091bf", + "libc-2.33.so+0x21613", + "php8.1+0x1093f7" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/python-3.10.12-nix-fib.json b/utils/coredump/testdata/arm64/python-3.10.12-nix-fib.json new file mode 100644 index 00000000..43c31066 --- /dev/null +++ b/utils/coredump/testdata/arm64/python-3.10.12-nix-fib.json @@ -0,0 +1,78 @@ +{ + "coredump-ref": "c93288784033ddea76690d3f041314f82db21710b1ab8abe2b8abe23766ca0ff", + "threads": [ + { + "lwp": 33070, + "frames": [ + "libpython3.10.so.1.0+0x6977c", + "libpython3.10.so.1.0+0xbdbd7", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "recur_fibo+1 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:4", + "+2 in /media/psf/Development/prodfiler/utils/coredump/testsources/python/./fib.py:3" + ] + } + ], + "modules": [ + { + "ref": "68071b1ae6ef41a68486d94c7d4935e8f6639186d58ae2ab42f41bf9e60db409", + "local-path": "/nix/store/izmxx6s0nl1mcwvl8a5wisckqd9vbmjw-python3-3.10.12/bin/python3.10" + }, + { + "ref": "6172458584fbf879f9048bb1513f999017f71cfe478ee8b42df31f8e995ddb9d", + "local-path": "/nix/store/rgizi393ki2q2p6lksk2rf53jgsjdvna-glibc-locales-2.37-8/lib/locale/locale-archive" + }, + { + "ref": "da5d4d024db9a259fb452e2e7ff8d56b758769d8813d0c94d6ca5d7a1c1614a1", + "local-path": "/nix/store/7zii1yvdbwchx06qz6sd82d1p53jx86y-glibc-2.37-8/lib/libc.so.6" + }, + { + "ref": "412c80537dbad3e1f9b73ba754dd7e3f60bf2a94e3bb07aac13cf98f3f7524e1", + "local-path": "/nix/store/cn95gf9d86wsj66aa1ahvbs7fkj3hgfp-gcc-12.3.0-libgcc/lib/libgcc_s.so.1" + }, + { + "ref": "642c6a6c4a4a2f49c2bf1bb410c2256f9541f022a8b2af9fb213612fdf3f039e", + "local-path": "/nix/store/7zii1yvdbwchx06qz6sd82d1p53jx86y-glibc-2.37-8/lib/libm.so.6" + }, + { + "ref": "a55fce5fe8901c7b53f792cded887db642289e0685e9e0e503c47b51f1e70c36", + "local-path": "/nix/store/7zii1yvdbwchx06qz6sd82d1p53jx86y-glibc-2.37-8/lib/libdl.so.2" + }, + { + "ref": "f9ad69590604af14b6d216360bde9e9ddc569579c02a14deb057c5b20e9cb16a", + "local-path": "/nix/store/i886g12blyyjyr8cbhl1iq7azw0hlxwp-libxcrypt-4.4.36/lib/libcrypt.so.2.0.0" + }, + { + "ref": "3e6ef8e2e7204b9e8380efa559469faf3e186cb8c78e12df6ce7863cd956b528", + "local-path": "/nix/store/izmxx6s0nl1mcwvl8a5wisckqd9vbmjw-python3-3.10.12/lib/libpython3.10.so.1.0" + }, + { + "ref": "e046e1c60cefe433aaded583d2ede85dfc91b4ed06234a91c264dc10f4257511", + "local-path": "/nix/store/7zii1yvdbwchx06qz6sd82d1p53jx86y-glibc-2.37-8/lib/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/python37.stringbench.10656.json b/utils/coredump/testdata/arm64/python37.stringbench.10656.json new file mode 100644 index 00000000..bf4f01bc --- /dev/null +++ b/utils/coredump/testdata/arm64/python37.stringbench.10656.json @@ -0,0 +1,19 @@ +{ + "coredump-ref": "0c46e3e8b635c3db2244eb7c56f56d0146f754ad4cdb8b86fc4259f2e5875f31", + "threads": [ + { + "lwp": 10656, + "frames": [ + "libpython3.7m.so.1.0+0xcd0f0", + "libpython3.7m.so.1.0+0x10652f", + "concat_many_strings+25 in stringbench.py:669", + "inner+4 in :6", + "timeit+14 in /usr/lib64/python3.7/timeit.py:177", + "best+8 in stringbench.py:1406", + "main+38 in stringbench.py:1447", + "+1477 in stringbench.py:1482" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/ruby-2.7.8p225-loop.json b/utils/coredump/testdata/arm64/ruby-2.7.8p225-loop.json new file mode 100644 index 00000000..73f9a72f --- /dev/null +++ b/utils/coredump/testdata/arm64/ruby-2.7.8p225-loop.json @@ -0,0 +1,102 @@ +{ + "coredump-ref": "6af1c283de22a67a60f1d148c7bd5306ebc1e84f83cbc8bda7fb53a549b2b309", + "threads": [ + { + "lwp": 36492, + "frames": [ + "libruby.so.2.7.8+0x175a30", + "libruby.so.2.7.8+0x212fe7", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.2.7.8+0x218017", + "libruby.so.2.7.8+0x223dd7", + "libruby.so.2.7.8+0x174be7", + "libruby.so.2.7.8+0x207ecf", + "libruby.so.2.7.8+0x202f17", + "libruby.so.2.7.8+0x215a23", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.2.7.8+0x218017", + "libruby.so.2.7.8+0x21ae23", + "libruby.so.2.7.8+0xa81cb", + "libruby.so.2.7.8+0xa83cb", + "libruby.so.2.7.8+0x207ecf", + "libruby.so.2.7.8+0x202f17", + "libruby.so.2.7.8+0x215a23", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.2.7.8+0x218017", + "libruby.so.2.7.8+0xa5d63", + "libruby.so.2.7.8+0xaa137", + "ruby+0xb3b", + "libc-2.31.so+0x20e17", + "ruby+0xb97" + ] + } + ], + "modules": [ + { + "ref": "d814c2cdf7113415c8970271ab1d6c01cfc70769081de1e7ee975f486e6c08d5", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "a61a32ad40890d1a08a9242386b4a8cba65ab8951432eb8f91feefe47842bef5", + "local-path": "/usr/local/lib/ruby/2.7.0/aarch64-linux/monitor.so" + }, + { + "ref": "2d849ac4c96f391e0cf354e513c87ff39b47266a0fdfd6973408ca063e6ac37a", + "local-path": "/usr/local/lib/ruby/2.7.0/aarch64-linux/enc/trans/transdb.so" + }, + { + "ref": "442756df7b621b39a0a9568d4929e28d02a40483f05c26724829f1078b7d30e5", + "local-path": "/usr/local/lib/ruby/2.7.0/aarch64-linux/enc/encdb.so" + }, + { + "ref": "2044fcff94037c2c75cc5bc3aa42877bf2b41042608e011706b6c631232b0edd", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "802de3cc336bf213c176cd845250a4d03146bc2e2acd5ea922fed79f2b6dafa0", + "local-path": "/lib/aarch64-linux-gnu/libm-2.31.so" + }, + { + "ref": "daec9eb17045e5dcb418926c75e7e0ddb94cc6694fe2a162ca990557571ab510", + "local-path": "/lib/aarch64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "decf9ee3921f890abab2cc427f31a2eaa9d07970fdcd7f4771cb76b7b71b2b65", + "local-path": "/lib/aarch64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "58e99cc018e8b3e5b5d15dc8a92751b9b885ce80afae47ff53c921f5c5f86458", + "local-path": "/usr/lib/aarch64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "31f866cf19794102cbccfe81c942ed90d9aa1e0886a87c2178b38219850c6c60", + "local-path": "/lib/aarch64-linux-gnu/librt-2.31.so" + }, + { + "ref": "6caaff1c1e689fda08ee4ca3fda2dc03f197bdec96f1deef9d826cceae4d426c", + "local-path": "/lib/aarch64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "7fb37d1568085015270b83b794d78bece261bdd617b664d2263780fd1093d5f4", + "local-path": "/lib/aarch64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "22092712a6b333a8e31f69b39a8526472bbd005d5474cfab3b10101c49b5cb6f", + "local-path": "/lib/aarch64-linux-gnu/libc-2.31.so" + }, + { + "ref": "b8ea3d046e9ed93ddfd9524577cb442c62589c1336e9dbe527bd4d798cc45e6a", + "local-path": "/usr/local/lib/libruby.so.2.7.8" + }, + { + "ref": "8ba2fddf055362d1be174e622659698d082830c47f99e9cea20f74d641553d97", + "local-path": "/lib/aarch64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/arm64/ruby-3.0.4p208-loop.json b/utils/coredump/testdata/arm64/ruby-3.0.4p208-loop.json new file mode 100644 index 00000000..ddf6b75e --- /dev/null +++ b/utils/coredump/testdata/arm64/ruby-3.0.4p208-loop.json @@ -0,0 +1,107 @@ +{ + "coredump-ref": "7b76cdf23ec39e19d6bf6447ee87020f4e2098671e7d9755f71d1ee70d46a57e", + "threads": [ + { + "lwp": 23794, + "frames": [ + "libruby.so.3.0.4+0x2a8670", + "libruby.so.3.0.4+0xeea1b", + "libruby.so.3.0.4+0xef02b", + "libruby.so.3.0.4+0x23831b", + "libruby.so.3.0.4+0x1729af", + "libruby.so.3.0.4+0x1c469b", + "libruby.so.3.0.4+0x28c1e7", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.0.4+0x291987", + "libruby.so.3.0.4+0x29df97", + "libruby.so.3.0.4+0x1c4373", + "libruby.so.3.0.4+0x27e697", + "libruby.so.3.0.4+0x28912f", + "libruby.so.3.0.4+0x28ef0b", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.0.4+0x2911df", + "libruby.so.3.0.4+0x2928a7", + "libruby.so.3.0.4+0xcc5db", + "libruby.so.3.0.4+0xcc80f", + "libruby.so.3.0.4+0x27e697", + "libruby.so.3.0.4+0x28912f", + "libruby.so.3.0.4+0x28ef0b", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.0.4+0x2911df", + "libruby.so.3.0.4+0xc9173", + "libruby.so.3.0.4+0xceeeb", + "ruby+0xb8b", + "libc-2.31.so+0x20e17", + "ruby+0xbe7" + ] + } + ], + "modules": [ + { + "ref": "5d40c0f2c1910c474c1be914bd92665ede870cea8a3c1a59a942c0e328ecb99d", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "1625e01413123b014da51cec0d46fd77149f1c12ed6dec5ce46c41ba494ef8c7", + "local-path": "/usr/local/lib/ruby/3.0.0/aarch64-linux/monitor.so" + }, + { + "ref": "72114b92bdca2d2bf3c271e0b4db9f62d4aa4526e10d3e474a56505a3b869587", + "local-path": "/usr/local/lib/ruby/3.0.0/aarch64-linux/enc/trans/transdb.so" + }, + { + "ref": "6452d6a0ef7c5cb088a818789059ffc87efc6066212a40ecc26d660d05e757de", + "local-path": "/usr/local/lib/ruby/3.0.0/aarch64-linux/enc/encdb.so" + }, + { + "ref": "2044fcff94037c2c75cc5bc3aa42877bf2b41042608e011706b6c631232b0edd", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "e7ff33fe09c75416e22aa74c0a22024b7b709c04512c6163ca65d1d5245d75ad", + "local-path": "/lib/aarch64-linux-gnu/libc-2.31.so" + }, + { + "ref": "faa394287347689f0a5f32711a86c51bce33865841e23c7d95c3e1034233d4a8", + "local-path": "/lib/aarch64-linux-gnu/libm-2.31.so" + }, + { + "ref": "daec9eb17045e5dcb418926c75e7e0ddb94cc6694fe2a162ca990557571ab510", + "local-path": "/lib/aarch64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "8a68bff83c8079ca027df33d70add7dca6b4379ed5b31afa5f72f67268b2ad2a", + "local-path": "/lib/aarch64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "58e99cc018e8b3e5b5d15dc8a92751b9b885ce80afae47ff53c921f5c5f86458", + "local-path": "/usr/lib/aarch64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "cf28b76891ee4ecd32aeec63d2ded7e4e1f5573c7c4688057f3d1a6e115608c9", + "local-path": "/lib/aarch64-linux-gnu/librt-2.31.so" + }, + { + "ref": "29db8bca17f39b763a3c33b649d8b86931ed50367a3cc83c0335a076fec6dc99", + "local-path": "/lib/aarch64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "7fb37d1568085015270b83b794d78bece261bdd617b664d2263780fd1093d5f4", + "local-path": "/lib/aarch64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "d64f1c54e9ade54017427655cf9dde444d1e33750d2db7534b98e41edbe1df2e", + "local-path": "/usr/local/lib/libruby.so.3.0.4" + }, + { + "ref": "585ea8ab582624fe720e73cdec311fdaf1c4ed481319929dd0ce4c3af8bd21c4", + "local-path": "/lib/aarch64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/arm64/ruby-3.0.6p216-loop.json b/utils/coredump/testdata/arm64/ruby-3.0.6p216-loop.json new file mode 100644 index 00000000..1688cb7c --- /dev/null +++ b/utils/coredump/testdata/arm64/ruby-3.0.6p216-loop.json @@ -0,0 +1,105 @@ +{ + "coredump-ref": "faeda38c6d3281eb8e8e447ef59601a63c7ba94d15f0d756bd294ff55f69300c", + "threads": [ + { + "lwp": 36175, + "frames": [ + "libruby.so.3.0.6+0x2952f0", + "libruby.so.3.0.6+0x1c59cf", + "libruby.so.3.0.6+0x27cab7", + "libruby.so.3.0.6+0x28754f", + "libruby.so.3.0.6+0x28d32b", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.0.6+0x28fda7", + "libruby.so.3.0.6+0x29bd67", + "libruby.so.3.0.6+0x1c5703", + "libruby.so.3.0.6+0x27cab7", + "libruby.so.3.0.6+0x28754f", + "libruby.so.3.0.6+0x28d32b", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.0.6+0x28f5ff", + "libruby.so.3.0.6+0x290cc7", + "libruby.so.3.0.6+0xcc60b", + "libruby.so.3.0.6+0xcc83f", + "libruby.so.3.0.6+0x27cab7", + "libruby.so.3.0.6+0x28754f", + "libruby.so.3.0.6+0x28d32b", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.0.6+0x28f5ff", + "libruby.so.3.0.6+0xc91a3", + "libruby.so.3.0.6+0xcef1b", + "ruby+0xb8b", + "libc-2.31.so+0x20e17", + "ruby+0xbe7" + ] + } + ], + "modules": [ + { + "ref": "ca30dd09fcd0b681ab5a24ffaa1dcd46d2232d063df65737bd7653586b4cf4a9", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "740e0e8d9903f3a242f7c80cc50fcd1618d79d6a5131dcb022db92e090293b38", + "local-path": "/usr/local/lib/ruby/3.0.0/aarch64-linux/monitor.so" + }, + { + "ref": "72114b92bdca2d2bf3c271e0b4db9f62d4aa4526e10d3e474a56505a3b869587", + "local-path": "/usr/local/lib/ruby/3.0.0/aarch64-linux/enc/trans/transdb.so" + }, + { + "ref": "492bdd35f99b603efdf65b82ebee55d7719793e81c2ab76cb1784ff0e45c9a2c", + "local-path": "/usr/local/lib/ruby/3.0.0/aarch64-linux/enc/encdb.so" + }, + { + "ref": "2044fcff94037c2c75cc5bc3aa42877bf2b41042608e011706b6c631232b0edd", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "22092712a6b333a8e31f69b39a8526472bbd005d5474cfab3b10101c49b5cb6f", + "local-path": "/lib/aarch64-linux-gnu/libc-2.31.so" + }, + { + "ref": "802de3cc336bf213c176cd845250a4d03146bc2e2acd5ea922fed79f2b6dafa0", + "local-path": "/lib/aarch64-linux-gnu/libm-2.31.so" + }, + { + "ref": "daec9eb17045e5dcb418926c75e7e0ddb94cc6694fe2a162ca990557571ab510", + "local-path": "/lib/aarch64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "decf9ee3921f890abab2cc427f31a2eaa9d07970fdcd7f4771cb76b7b71b2b65", + "local-path": "/lib/aarch64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "58e99cc018e8b3e5b5d15dc8a92751b9b885ce80afae47ff53c921f5c5f86458", + "local-path": "/usr/lib/aarch64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "31f866cf19794102cbccfe81c942ed90d9aa1e0886a87c2178b38219850c6c60", + "local-path": "/lib/aarch64-linux-gnu/librt-2.31.so" + }, + { + "ref": "6caaff1c1e689fda08ee4ca3fda2dc03f197bdec96f1deef9d826cceae4d426c", + "local-path": "/lib/aarch64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "7fb37d1568085015270b83b794d78bece261bdd617b664d2263780fd1093d5f4", + "local-path": "/lib/aarch64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "09bab7e90218fe73cdfe6aa6da9ee97dde213f11509331dc273fc5795436ae14", + "local-path": "/usr/local/lib/libruby.so.3.0.6" + }, + { + "ref": "8ba2fddf055362d1be174e622659698d082830c47f99e9cea20f74d641553d97", + "local-path": "/lib/aarch64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/arm64/ruby-3.1.0p0-loop.json b/utils/coredump/testdata/arm64/ruby-3.1.0p0-loop.json new file mode 100644 index 00000000..95ee8846 --- /dev/null +++ b/utils/coredump/testdata/arm64/ruby-3.1.0p0-loop.json @@ -0,0 +1,102 @@ +{ + "coredump-ref": "ae23a18eb60acc98ffd5ed8302f08aed0ee9738ada1427c94ce50233fdf6c2c3", + "threads": [ + { + "lwp": 40045, + "frames": [ + "libruby.so.3.1.0+0x28c3e8", + "libruby.so.3.1.0+0x29ab13", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.1.0+0x2a068b", + "libruby.so.3.1.0+0x2a49cb", + "libruby.so.3.1.0+0x1d4833", + "libruby.so.3.1.0+0x28c3f7", + "libruby.so.3.1.0+0x2919e3", + "libruby.so.3.1.0+0x29d83f", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.1.0+0x29feff", + "libruby.so.3.1.0+0x2a124b", + "libruby.so.3.1.0+0xd1c0b", + "libruby.so.3.1.0+0xd1e3f", + "libruby.so.3.1.0+0x28c3f7", + "libruby.so.3.1.0+0x2919e3", + "libruby.so.3.1.0+0x29d83f", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.1.0+0x29feff", + "libruby.so.3.1.0+0xce227", + "libruby.so.3.1.0+0xd43c7", + "ruby+0xb8b", + "libc-2.31.so+0x24217", + "ruby+0xbe7" + ] + } + ], + "modules": [ + { + "ref": "7ad4307bc489a6787ea146738e5c1af82df611b983fd881b9a659c1878ac160a", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "427481a55670499152d97842071c9866deb6c4a7e210075ee95588d965a6a653", + "local-path": "/usr/local/lib/ruby/3.1.0/aarch64-linux/monitor.so" + }, + { + "ref": "f5cc0ef585910a209c108b36a14dadb7b28de044e6e6b7a0cd22084d8dff9547", + "local-path": "/usr/local/lib/ruby/3.1.0/aarch64-linux/enc/trans/transdb.so" + }, + { + "ref": "c4aedce62ed7c7a2a2d5209f2445719072ff776f30d62c01db1008ad7cdbb929", + "local-path": "/usr/local/lib/ruby/3.1.0/aarch64-linux/enc/encdb.so" + }, + { + "ref": "2044fcff94037c2c75cc5bc3aa42877bf2b41042608e011706b6c631232b0edd", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "e651767dc41e949ff64d32c9ce10e78f9cad510a3e84c14e5069f9900578d44a", + "local-path": "/lib/aarch64-linux-gnu/libc-2.31.so" + }, + { + "ref": "e7df604d36b489af20d2e22c7924d6d855646780301de7fff647353cb14a6282", + "local-path": "/lib/aarch64-linux-gnu/libm-2.31.so" + }, + { + "ref": "daec9eb17045e5dcb418926c75e7e0ddb94cc6694fe2a162ca990557571ab510", + "local-path": "/lib/aarch64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "fc77f71dc2ac00b70fbefd7b7038a55c891d01a43cba1373f7cd5eaa15b1e561", + "local-path": "/lib/aarch64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "58e99cc018e8b3e5b5d15dc8a92751b9b885ce80afae47ff53c921f5c5f86458", + "local-path": "/usr/lib/aarch64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "42b19e7c0bd19a4e595433f86446bf2dd70c61eee5a90cecb1d6ddf7ced9e227", + "local-path": "/lib/aarch64-linux-gnu/librt-2.31.so" + }, + { + "ref": "7bb4770caf30f9b69ad13dd44ef36a12b87c574162acf52f9ddd487715faa0c8", + "local-path": "/lib/aarch64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "d7e452e38e2b8d3f47d6110e50a1c0b505477d558e56806cccc75535d0302393", + "local-path": "/lib/aarch64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "cba1f276477d79e0d434a96bad98ca5ce9862bb700d50431170e34e9fa0b6f31", + "local-path": "/usr/local/lib/libruby.so.3.1.0" + }, + { + "ref": "5ef7a5fc86d7110aad7be60ac7a3a89669eefdafbaed239ca400d190faec6402", + "local-path": "/lib/aarch64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/arm64/ruby-3.1.4p223-loop.json b/utils/coredump/testdata/arm64/ruby-3.1.4p223-loop.json new file mode 100644 index 00000000..f10ac689 --- /dev/null +++ b/utils/coredump/testdata/arm64/ruby-3.1.4p223-loop.json @@ -0,0 +1,109 @@ +{ + "coredump-ref": "2bb49365d369e02150f55c39b178308c12a90c451dc8bcdcc396d313592a0428", + "threads": [ + { + "lwp": 40655, + "frames": [ + "libruby.so.3.1.4+0x297464", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:12", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.1.4+0x29b6ff", + "libruby.so.3.1.4+0x2a018b", + "libruby.so.3.1.4+0x1d0fbf", + "libruby.so.3.1.4+0x287c87", + "libruby.so.3.1.4+0x28d203", + "libruby.so.3.1.4+0x299067", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.1.4+0x29be4b", + "libruby.so.3.1.4+0x2a018b", + "libruby.so.3.1.4+0x1d0d83", + "libruby.so.3.1.4+0x287c87", + "libruby.so.3.1.4+0x28d203", + "libruby.so.3.1.4+0x299067", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.1.4+0x29b6ff", + "libruby.so.3.1.4+0x29ca0b", + "libruby.so.3.1.4+0xcfdbb", + "libruby.so.3.1.4+0xcffef", + "libruby.so.3.1.4+0x287c87", + "libruby.so.3.1.4+0x28d203", + "libruby.so.3.1.4+0x299067", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.1.4+0x29b6ff", + "libruby.so.3.1.4+0xcc2f7", + "libruby.so.3.1.4+0xd2577", + "ruby+0xb8b", + "libc-2.31.so+0x20e17", + "ruby+0xbe7" + ] + } + ], + "modules": [ + { + "ref": "91e420adcbcde0ecf27b779990044db04e1b7b3340a539ec9d672fa50b4acccb", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "986f28fb12239163cc852423409e53460f15e750b179ebef75405a2584b8def5", + "local-path": "/usr/local/lib/ruby/3.1.0/aarch64-linux/monitor.so" + }, + { + "ref": "0095f138efb823d36176e0fa1763e045ed99da4c4351a67887ec1d2e5beb597f", + "local-path": "/usr/local/lib/ruby/3.1.0/aarch64-linux/enc/trans/transdb.so" + }, + { + "ref": "829c4db114763b5862fbb77ccb7eb03534abf2e04f7e482e15b3cc98a634534f", + "local-path": "/usr/local/lib/ruby/3.1.0/aarch64-linux/enc/encdb.so" + }, + { + "ref": "2044fcff94037c2c75cc5bc3aa42877bf2b41042608e011706b6c631232b0edd", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "e19b13d271385be968316e97e08ef7a33d7702b8c370e93dbd9e7470933411c7", + "local-path": "/usr/lib/locale/C.UTF-8/LC_CTYPE" + }, + { + "ref": "22092712a6b333a8e31f69b39a8526472bbd005d5474cfab3b10101c49b5cb6f", + "local-path": "/lib/aarch64-linux-gnu/libc-2.31.so" + }, + { + "ref": "802de3cc336bf213c176cd845250a4d03146bc2e2acd5ea922fed79f2b6dafa0", + "local-path": "/lib/aarch64-linux-gnu/libm-2.31.so" + }, + { + "ref": "daec9eb17045e5dcb418926c75e7e0ddb94cc6694fe2a162ca990557571ab510", + "local-path": "/lib/aarch64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "decf9ee3921f890abab2cc427f31a2eaa9d07970fdcd7f4771cb76b7b71b2b65", + "local-path": "/lib/aarch64-linux-gnu/libdl-2.31.so" + }, + { + "ref": "58e99cc018e8b3e5b5d15dc8a92751b9b885ce80afae47ff53c921f5c5f86458", + "local-path": "/usr/lib/aarch64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "31f866cf19794102cbccfe81c942ed90d9aa1e0886a87c2178b38219850c6c60", + "local-path": "/lib/aarch64-linux-gnu/librt-2.31.so" + }, + { + "ref": "6caaff1c1e689fda08ee4ca3fda2dc03f197bdec96f1deef9d826cceae4d426c", + "local-path": "/lib/aarch64-linux-gnu/libpthread-2.31.so" + }, + { + "ref": "7fb37d1568085015270b83b794d78bece261bdd617b664d2263780fd1093d5f4", + "local-path": "/lib/aarch64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "f3c230fc6ebc69b8b1f7317440f8429f94eb4f5e3ee3ca3503e6013ac96ea585", + "local-path": "/usr/local/lib/libruby.so.3.1.4" + }, + { + "ref": "8ba2fddf055362d1be174e622659698d082830c47f99e9cea20f74d641553d97", + "local-path": "/lib/aarch64-linux-gnu/ld-2.31.so" + } + ] +} diff --git a/utils/coredump/testdata/arm64/ruby-3.2.2-loop.json b/utils/coredump/testdata/arm64/ruby-3.2.2-loop.json new file mode 100644 index 00000000..6e83dd73 --- /dev/null +++ b/utils/coredump/testdata/arm64/ruby-3.2.2-loop.json @@ -0,0 +1,98 @@ +{ + "coredump-ref": "97e9bac14375fea16557e69d30ab3de4577e59f620d727ed0a195dd802b13609", + "threads": [ + { + "lwp": 220805, + "frames": [ + "libruby.so.3.2.2+0xd65e4", + "libruby.so.3.2.2+0x255d6f", + "libruby.so.3.2.2+0x31f0bf", + "libruby.so.3.2.2+0x3250fb", + "libruby.so.3.2.2+0x32f0c7", + "is_prime+0 in /pwd/testsources/ruby/loop.rb:10", + "sum_of_primes+0 in /pwd/testsources/ruby/loop.rb:20", + "
+0 in /pwd/testsources/ruby/loop.rb:30", + "libruby.so.3.2.2+0x33418f", + "libruby.so.3.2.2+0x3381d7", + "libruby.so.3.2.2+0x255b93", + "libruby.so.3.2.2+0x31f0bf", + "libruby.so.3.2.2+0x3250fb", + "libruby.so.3.2.2+0x32f0c7", + "
+0 in /pwd/testsources/ruby/loop.rb:29", + "libruby.so.3.2.2+0x333e17", + "libruby.so.3.2.2+0x335277", + "libruby.so.3.2.2+0x152473", + "libruby.so.3.2.2+0x1526ab", + "libruby.so.3.2.2+0x31f0bf", + "libruby.so.3.2.2+0x3250fb", + "libruby.so.3.2.2+0x32f0c7", + "
+0 in /pwd/testsources/ruby/loop.rb:28", + "libruby.so.3.2.2+0x333e17", + "libruby.so.3.2.2+0x14e003", + "libruby.so.3.2.2+0x154677", + "ruby+0xb2b", + "libc.so.6+0x2777f", + "libc.so.6+0x27857", + "ruby+0xbaf" + ] + } + ], + "modules": [ + { + "ref": "969fe88169ba3d774d8637e9229fa5ed6ba73eb1a2b81ca8069f1ebc985ef203", + "local-path": "/usr/local/bin/ruby" + }, + { + "ref": "c90e436f50748a03aa6499d1d7e87a17571c316dff26b35795cdd52c81a015c2", + "local-path": "/usr/local/lib/ruby/3.2.0/aarch64-linux/monitor.so" + }, + { + "ref": "894767d90101122dc073791a7e3cdcb528ad5a7e5334a12bd93d1b4d590c87c9", + "local-path": "/usr/local/lib/ruby/3.2.0/aarch64-linux/enc/trans/transdb.so" + }, + { + "ref": "4b97951caabf7726ba9aa57884c12e5ddff3894aebbee4f13e08d667cbf45acb", + "local-path": "/usr/local/lib/ruby/3.2.0/aarch64-linux/enc/encdb.so" + }, + { + "ref": "e4b5576b19e40be5923b0eb864750d35944404bb0a92aa68d1a9b96110c52120", + "local-path": "/usr/lib/locale/C.utf8/LC_CTYPE" + }, + { + "ref": "8526cf244c909542d584aa8454212341290a4537020b9bd30c7f2d3f7c1bc308", + "local-path": "/usr/lib/aarch64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "e8ccdb5ffa5e29c0692e5afcb4e1f3f536067bb261e79e8a089a163d0fafdd45", + "local-path": "/usr/lib/aarch64-linux-gnu/libc.so.6" + }, + { + "ref": "ae6bd25b1f9616e37fb652d1052af984576d22adacfd3bced9df82d075ad92c7", + "local-path": "/usr/lib/aarch64-linux-gnu/libm.so.6" + }, + { + "ref": "3c70a152ded3b0eab8f2f3b3d2a80e2ce0808909e8c6828a8dcfc6b883ff30fb", + "local-path": "/usr/lib/aarch64-linux-gnu/libcrypt.so.1.1.0" + }, + { + "ref": "391b336ee37cd47a1d4748ba2d15bd125c1a082864f7bca9c92612f8a0ac305e", + "local-path": "/usr/lib/aarch64-linux-gnu/libgmp.so.10.4.1" + }, + { + "ref": "ffb1ab496e6eced03ab679075f9f2c415c7728a145cc7f63d614497102d73822", + "local-path": "/usr/lib/aarch64-linux-gnu/libz.so.1.2.13" + }, + { + "ref": "4d3221ae78a31f3c04fb94dcc164afb56c8c2d276e3e5b8689a78a8940583086", + "local-path": "/usr/local/lib/libruby.so.3.2.2" + }, + { + "ref": "c4c52c0df6f3809f1b85927a85f322d102cffebb6b517b44b37048f6fd620c01", + "local-path": "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache" + }, + { + "ref": "5ece65ae57fce3774af5218e7c3de93f1cc79f70b92ae3bec86a4135fc2396c2", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/arm64/stackalign.19272.json b/utils/coredump/testdata/arm64/stackalign.19272.json new file mode 100644 index 00000000..a0613851 --- /dev/null +++ b/utils/coredump/testdata/arm64/stackalign.19272.json @@ -0,0 +1,15 @@ +{ + "coredump-ref": "8e03102e5ff19676e3fdd3ad942b9d9e085c2c29e32f4c834cb0ffe1e09a537f", + "threads": [ + { + "lwp": 19272, + "frames": [ + "stackalign+0x860", + "stackalign+0x70f", + "libc-2.33.so+0x24ad3", + "stackalign+0x777" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/arm64/stackalign.67475.json b/utils/coredump/testdata/arm64/stackalign.67475.json new file mode 100644 index 00000000..ac17075a --- /dev/null +++ b/utils/coredump/testdata/arm64/stackalign.67475.json @@ -0,0 +1,32 @@ +{ + "coredump-ref": "4533ef3d0fdf4e16f093e7aa26e14d26da3e9ba9779b4e86cf446143361f9986", + "threads": [ + { + "lwp": 67475, + "frames": [ + "libc-2.33.so+0xa4b60", + "stackalign+0x95f", + "stackalign+0x96f", + "stackalign+0x96f", + "stackalign+0x96f", + "stackalign+0x70f", + "libc-2.33.so+0x21613", + "stackalign+0x777" + ] + } + ], + "modules": [ + { + "ref": "b95a10cd6c5a218f195287bda176bc162091b2aeaeab7f0fae45fed8d4075c0e", + "local-path": "/media/psf/devel/prodfiler/utils/coredump/testsources/c/stackalign" + }, + { + "ref": "1159053678e9e948b064a36d9abad8592e195a4815a3ecfddd6c4c6da86fea54", + "local-path": "/usr/lib/aarch64-linux-gnu/libc-2.33.so" + }, + { + "ref": "c2bed948068b2fdbfb2daf41c7a25c4cbe69e40323bb9761061f93157941fb9c", + "local-path": "/usr/lib/aarch64-linux-gnu/ld-2.33.so" + } + ] +} diff --git a/utils/coredump/testdata/arm64/stackalign.7265.json b/utils/coredump/testdata/arm64/stackalign.7265.json new file mode 100644 index 00000000..8ee02121 --- /dev/null +++ b/utils/coredump/testdata/arm64/stackalign.7265.json @@ -0,0 +1,19 @@ +{ + "coredump-ref": "3effcb31df636723dcfb07ccf2ab0dfd0ac093ff1fd1911dd67c50858e41d876", + "threads": [ + { + "lwp": 7265, + "frames": [ + "libc-2.33.so+0xa8020", + "stackalign+0x95f", + "stackalign+0x96f", + "stackalign+0x96f", + "stackalign+0x96f", + "stackalign+0x70f", + "libc-2.33.so+0x24ad3", + "stackalign+0x777" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testsources/c/brokenstack.c b/utils/coredump/testsources/c/brokenstack.c new file mode 100644 index 00000000..2e71da79 --- /dev/null +++ b/utils/coredump/testsources/c/brokenstack.c @@ -0,0 +1,32 @@ +// Example application that intentionally breaks its stack. +// +// cc -O2 -g -o brokenstack brokenstack.c + +#include + +#define FORCE_FRAME \ + __attribute__((noinline)) \ + __attribute__((optimize("no-omit-frame-pointer"))) \ + __attribute__((optimize("no-optimize-sibling-calls"))) + +static volatile int cond = 1; + +FORCE_FRAME void a() { + while(cond); +} + +FORCE_FRAME void b() { + a(); +} + +FORCE_FRAME void c() { + uint64_t* frame = __builtin_frame_address(0); + frame[0] = 0x42; + frame[1] = 0x42; + b(); +} + +int main() { + c(); +} + diff --git a/utils/coredump/testsources/c/sig.c b/utils/coredump/testsources/c/sig.c new file mode 100644 index 00000000..5a5f4e2e --- /dev/null +++ b/utils/coredump/testsources/c/sig.c @@ -0,0 +1,22 @@ +#include +#include +#include + +unsigned long special_symbol = 0xdeadbeef; + +void sig_handler(int signo) +{ + if (signo == SIGINT) + printf("received SIGINT\n"); + sleep(10); +} + +int main(void) +{ + if (signal(SIGINT, sig_handler) == SIG_ERR) + printf("\ncan't catch SIGINT\n"); + // A long long wait so that we can easily issue a signal to this process + while(1) + sleep(1); + return 0; +} diff --git a/utils/coredump/testsources/c/stackalign.c b/utils/coredump/testsources/c/stackalign.c new file mode 100644 index 00000000..ea514f27 --- /dev/null +++ b/utils/coredump/testsources/c/stackalign.c @@ -0,0 +1,33 @@ +// gcc -O3 -fomit-frame-pointer -mavx -ftree-vectorize stackalign.c -o stackalign + +#include +#include + +int calc(int r) +{ + const int N = 2000; //Array Size + const int noTests = 10000; //Number of tests + float a[N],b[N],c[N],result[N]; + + for (int i = 0; i < N; ++i) { + a[i] = ((float)i)+ 0.1335f; + b[i] = 1.50f*((float)i)+ 0.9383f; + c[i] = 0.33f*((float)i)+ 0.1172f; + } + + for (int i = 0; i < noTests; ++i) + for (int j = 0; j < N; ++j) + result[j] = a[j]+b[j]-c[j]+3*(float)i; + + while (r == 3) pause(); + + calc(r+1); + + fprintf(stderr, "calc(%d) done\n", r); +} + +int main(void) +{ + calc(0); + return 0; +} diff --git a/utils/coredump/testsources/go/hello.go b/utils/coredump/testsources/go/hello.go new file mode 100644 index 00000000..98b6898a --- /dev/null +++ b/utils/coredump/testsources/go/hello.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 main + +import "fmt" + +//go:noinline +func leaf() {} + +//go:noinline +func hello() { + fmt.Println("hello world!") + hello1() +} + +//go:noinline +func hello1() { + hello2() + fmt.Println("hello world!") +} + +//go:noinline +func hello2() { + fmt.Println("hello world!") + x := make([]uint32, 3345) + hello3(x) +} + +//go:noinline +func hello3(x []uint32) { + hello4() + fmt.Printf("hello world! %x", x[2234]) +} + +//go:noinline +func hello4() { + hello5() + fmt.Println("hello world!") +} + +//go:noinline +func hello5() { + fmt.Println("hello world!") + leaf() +} + +func main() { + hello() +} diff --git a/utils/coredump/testsources/graalvm/.gitignore b/utils/coredump/testsources/graalvm/.gitignore new file mode 100644 index 00000000..ed531e4c --- /dev/null +++ b/utils/coredump/testsources/graalvm/.gitignore @@ -0,0 +1,3 @@ +co +hellograal +hellograal.* diff --git a/utils/coredump/testsources/graalvm/HelloGraal.java b/utils/coredump/testsources/graalvm/HelloGraal.java new file mode 100644 index 00000000..f2f53f54 --- /dev/null +++ b/utils/coredump/testsources/graalvm/HelloGraal.java @@ -0,0 +1,23 @@ +package co.elastic.profiling; + +import java.security.SecureRandom; + +public class HelloGraal { + + public static void main(String[] args) { + SecureRandom rand = new SecureRandom(); + long maxCounter = 10000000l; + + for (long i = 0; i < maxCounter; i++) { + // trigger page fault + byte[] randomBytes = new byte[4444]; + rand.nextBytes(randomBytes); + + if (i > 0 && (maxCounter - i) % 10000 == 0) { + System.out.printf("Progress reading from rand: %d out of %d %n", i , maxCounter); + } + } + + System.out.println("Done!"); + } +} diff --git a/utils/coredump/testsources/graalvm/Makefile b/utils/coredump/testsources/graalvm/Makefile new file mode 100644 index 00000000..d5e6156a --- /dev/null +++ b/utils/coredump/testsources/graalvm/Makefile @@ -0,0 +1,8 @@ +build-java: + @javac -d . HelloGraal.java + +build-executable: build-java + @native-image -cp . co.elastic.profiling.HelloGraal hellograal + +clean: + @rm -rf hellograal co diff --git a/utils/coredump/testsources/graalvm/README.md b/utils/coredump/testsources/graalvm/README.md new file mode 100644 index 00000000..6e1a3f8f --- /dev/null +++ b/utils/coredump/testsources/graalvm/README.md @@ -0,0 +1,17 @@ +## Testing GraalVM native images + +### Pre-requisites + +1. [download and install GraalVM for Linux](https://www.graalvm.org/22.3/docs/getting-started/linux/) +2. setup GraalVM as default Java runtime +3. install the `native-image` compiler, see more details in the + [docs](https://www.graalvm.org/22.3/reference-manual/native-image/#install-native-image) + +### Run the experiment + +- Build a native image executable from the code in HelloGraal.java + ```bash + make build-executable + ``` +- Start the host-agent +- Run the `hellograal` binary diff --git a/utils/coredump/testsources/java/Deopt.java b/utils/coredump/testsources/java/Deopt.java new file mode 100644 index 00000000..0d0efc2c --- /dev/null +++ b/utils/coredump/testsources/java/Deopt.java @@ -0,0 +1,34 @@ +// Works together with DeoptFoo, and triggers deoptimized frames to be seen on the stack + +class Deopt { + public int foo; + + public void Bar() { + foo = foo * 123; + } + + public void Handle(int x) { + for (int i = 0; i < 1000000000; i++) { + Bar(); + } + if (x < 10) { + Handle(x + 1); + } else { + try { + ClassLoader.getSystemClassLoader().loadClass("DeoptFoo"); + while (true) { + System.out.print("foo\n"); + } + } catch (Exception e) { + } + } + } + + public static void main( String []args ) throws InterruptedException { + Deopt foo = new Deopt(); + foo.foo = 2; + while (true) { + foo.Handle(1); + } + } +} diff --git a/utils/coredump/testsources/java/DeoptFoo.java b/utils/coredump/testsources/java/DeoptFoo.java new file mode 100644 index 00000000..a5a24a85 --- /dev/null +++ b/utils/coredump/testsources/java/DeoptFoo.java @@ -0,0 +1,6 @@ +class DeoptFoo extends Deopt { + public void Bar() { + foo = foo * 321; + } +} + diff --git a/utils/coredump/testsources/java/HelloWorld.java b/utils/coredump/testsources/java/HelloWorld.java new file mode 100644 index 00000000..7e0bb36c --- /dev/null +++ b/utils/coredump/testsources/java/HelloWorld.java @@ -0,0 +1,8 @@ +class HelloWorld { + public static void main( String []args ) throws InterruptedException { + while (true) { + System.out.println( "Hello World!" ); + //Thread.sleep(10); + } + } +} diff --git a/utils/coredump/testsources/java/Lambda1.java b/utils/coredump/testsources/java/Lambda1.java new file mode 100644 index 00000000..50b7770a --- /dev/null +++ b/utils/coredump/testsources/java/Lambda1.java @@ -0,0 +1,23 @@ +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.lang.Thread; + +public class Lambda1 { + public static Comparator comparator1() { + return (d1, d2) -> { + try { + Thread.sleep(10000); // generate core from this location + } catch (Exception e) { + } + return d1.compareTo(d2); + }; + } + + public static void main(String[] args) { + ArrayList list = new ArrayList<>(); + list.add(1.9); + list.add(1.2); + Collections.sort(list, comparator1()); + } +} diff --git a/utils/coredump/testsources/java/Prof1.java b/utils/coredump/testsources/java/Prof1.java new file mode 100644 index 00000000..b4f18fe6 --- /dev/null +++ b/utils/coredump/testsources/java/Prof1.java @@ -0,0 +1,12 @@ + +public class Prof1 { + + public static void main(String[] args) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000000; i++) { + sb.append("ab"); + sb.delete(0, 1); + } + System.out.println(sb.length()); + } +} diff --git a/utils/coredump/testsources/java/Prof2.java b/utils/coredump/testsources/java/Prof2.java new file mode 100644 index 00000000..476ecf5f --- /dev/null +++ b/utils/coredump/testsources/java/Prof2.java @@ -0,0 +1,19 @@ +// Triggers "vtable chunks" frames + +import java.util.function.Supplier; + +public class Prof2 { + + public static void main(String[] args) { + Supplier[] suppliers = { + () -> 0, + () -> 1.0, + () -> "abc", + () -> true + }; + + for (int i = 0; i >= 0; i++) { + suppliers[i % suppliers.length].get(); + } + } +} diff --git a/utils/coredump/testsources/java/PrologueEpilogue.java b/utils/coredump/testsources/java/PrologueEpilogue.java new file mode 100644 index 00000000..9fc0cd27 --- /dev/null +++ b/utils/coredump/testsources/java/PrologueEpilogue.java @@ -0,0 +1,31 @@ +/* +gdb -x ./javagdbinit --args java -Xcomp \ + -XX:+UnlockDiagnosticVMOptions \ + "-XX:CompileCommand=dontinline *PrologueEpilogue.*" \ + "-XX:CompileCommand=BreakAtExecute *PrologueEpilogue.*" \ + "-XX:CompileCommand=compileonly *PrologueEpilogue.*" \ + -XX:+PrintOptoAssembly -XX:+PrintAssembly -XX:-UseOnStackReplacement \ + -XX:-TieredCompilation PrologueEpilogue +*/ + +class PrologueEpilogue { + static int a() { + return b(); + } + + static int b() { + return c(); + } + + static int c() { + return 42; + } + + public static void main(String [] argv) { + int ctr = 123; + for (long i = 0; i < 100; ++i) { + ctr += a(); + } + System.out.println(ctr); + } +} diff --git a/utils/coredump/testsources/java/ShaShenanigans.java b/utils/coredump/testsources/java/ShaShenanigans.java new file mode 100644 index 00000000..83e84086 --- /dev/null +++ b/utils/coredump/testsources/java/ShaShenanigans.java @@ -0,0 +1,38 @@ +// Reduced variant of Jonas Kunz' CPU burner application. Pressures SHA256 +// which is implemented via StubRoutines. + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +public class ShaShenanigans { + public static volatile Object sink; + + public static void main(String[] args) { + while (true) { + shaShenanigans(); + } + } + + public static void shaShenanigans() { + long start = System.nanoTime(); + while ((System.nanoTime() - start) < 100_000_000L) { + sink = hashRandomStuff(); + } + } + + private static byte[] hashRandomStuff() { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + Random rnd = new Random(); + byte[] buffer = new byte[1024]; + rnd.nextBytes(buffer); + for(int i=0; i<5000; i++) { + digest.update(buffer); + } + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/utils/coredump/testsources/java/VdsoPressure.java b/utils/coredump/testsources/java/VdsoPressure.java new file mode 100644 index 00000000..9c468a7b --- /dev/null +++ b/utils/coredump/testsources/java/VdsoPressure.java @@ -0,0 +1,12 @@ +import java.lang.System; + +class VdsoPressure { + public static void main(String [] argv) { + long ctr = 0; + for (long i = 0; i < 10_000_000_000L; ++i) { + ctr += System.nanoTime(); + } + System.out.println(ctr); + } +} + diff --git a/utils/coredump/testsources/java/javagdbinit b/utils/coredump/testsources/java/javagdbinit new file mode 100644 index 00000000..05d1fe36 --- /dev/null +++ b/utils/coredump/testsources/java/javagdbinit @@ -0,0 +1,3 @@ +handle SIGILL nostop +handle SIGSEGV nostop +set pagination off diff --git a/utils/coredump/testsources/node/async.js b/utils/coredump/testsources/node/async.js new file mode 100644 index 00000000..18a70295 --- /dev/null +++ b/utils/coredump/testsources/node/async.js @@ -0,0 +1,50 @@ +const meetCustomer = (id) => { +  return new Promise((resolve, reject) => { +    setTimeout(() => { +      console.log(`Waiter approached customer at table #${id}...`); +      resolve({ customerId: id }); +    }, id); +  }); +} +const getOrder = (id) => { +  return new Promise((resolve, reject) => { +    setTimeout(() => { +      console.log(`Order Received for customer at table #${id}...`); +      resolve({ customerId: id, customerOrder: "Pizza" }); +    }, 1); +  }); +} +const notifyWaiter = (id) => { +  return new Promise((resolve, reject) => { +    setTimeout(() => { +      console.log(`Order for customer at table #${id} processed....`); +      resolve({ customerId: id, customerOrder: "Pizza" }); +      // reject(new Error("Error occured with waiter")); +    }, 1); +  }); +} +const serveCustomer = (id) => { +  return new Promise((resolve, reject) => { + for (let foo=0; foo <1000; foo++) { + console.trace("I am here"); + } +    setTimeout(() => { +      console.log(`Customer with order number #${id} served...`); +      resolve({ customerId: id, customerOrder: "Pizza" }); +    }, id); +  }); +} + +// Async- await approach +const runRestaurant = async (customerId) => { +  const customer = await meetCustomer(customerId) +  const order = await getOrder(customer.customerId) +  await notifyWaiter(order.customerId) +  await serveCustomer(order.customerId) + console.log(`Order of customer fulfilled...`) +} + +for (let i = 0; i < 1000; i++) { +  console.log(`Order Received for customer at table #${i}...`); + runRestaurant(i); +} diff --git a/utils/coredump/testsources/node/hello.js b/utils/coredump/testsources/node/hello.js new file mode 100644 index 00000000..a63ec53b --- /dev/null +++ b/utils/coredump/testsources/node/hello.js @@ -0,0 +1,15 @@ +function bar(a, s) { + a(s) + //ads +} + +function foo() { + bar(a, "Hello world!") +} + +console.trace("I am here"); +a = console.log +for (i = 0; i < 10000000; i++) { + foo() +} + diff --git a/utils/coredump/testsources/node/hello2.js b/utils/coredump/testsources/node/hello2.js new file mode 100644 index 00000000..1134886f --- /dev/null +++ b/utils/coredump/testsources/node/hello2.js @@ -0,0 +1,21 @@ +function bar(a, s) { + a(s) + //ads +} + +function foo() { + bar(a, "Hello world!") +} + +function doit() { + for (i = 0; i < 1000; i++) { + foo() + } +} + +console.trace("I am here"); +a = console.log +for (j = 0; j < 1000; j++) { + doit() +} + diff --git a/utils/coredump/testsources/node/inlining.js b/utils/coredump/testsources/node/inlining.js new file mode 100644 index 00000000..9ee6c0f3 --- /dev/null +++ b/utils/coredump/testsources/node/inlining.js @@ -0,0 +1,27 @@ + +function add(a, b) { + console.trace("here") + return a + b +} + +function add3(a, b, c) { + return add(a, add(b, c)) +} + +function test(a, b, c, d) { + return add3(a, b, c) == d +} + +function submain() { + for (var i = 0; i < 1000; i++) { + test(i, 2*i, 3*i, 100) + } +} + +function main() { + for (var i = 0; i < 1000; i++) { + submain() + } +} + +main() diff --git a/utils/coredump/testsources/node/test.js b/utils/coredump/testsources/node/test.js new file mode 100644 index 00000000..9ee6c0f3 --- /dev/null +++ b/utils/coredump/testsources/node/test.js @@ -0,0 +1,27 @@ + +function add(a, b) { + console.trace("here") + return a + b +} + +function add3(a, b, c) { + return add(a, add(b, c)) +} + +function test(a, b, c, d) { + return add3(a, b, c) == d +} + +function submain() { + for (var i = 0; i < 1000; i++) { + test(i, 2*i, 3*i, 100) + } +} + +function main() { + for (var i = 0; i < 1000; i++) { + submain() + } +} + +main() diff --git a/utils/coredump/testsources/perl/a.pl b/utils/coredump/testsources/perl/a.pl new file mode 100644 index 00000000..75a26a29 --- /dev/null +++ b/utils/coredump/testsources/perl/a.pl @@ -0,0 +1,32 @@ +#!/usr/bin/perl + +# Perl Program to calculate Fibonancci +sub fib +{ + +# Retrieving the first argument +# passed with function calling +my $x = $_[0]; + +# checking if that value is 0 or 1 +if ($x == 0 || $x == 1) +{ + return 1; +} + +# Recursively calling function with the next value +# which is one less than current one +else +{ + if ($x == 20) { + sleep 100; + } + return $x + fib($x - 1); +} +} + +# Driver Code +$a = 30; + +# Function call and printing result after return +print "Fibonancci of a number $a is ", fib($a), "\n"; diff --git a/utils/coredump/testsources/perl/hi.pl b/utils/coredump/testsources/perl/hi.pl new file mode 100644 index 00000000..94a925a6 --- /dev/null +++ b/utils/coredump/testsources/perl/hi.pl @@ -0,0 +1,20 @@ +package HelloWorld; +sub new +{ + my $class = shift; + my $self = { }; + bless $self, $class; + return $self; +} +sub print +{ + eval { + print "Hello World!\n"; + } +} + +package main; +$hw = HelloWorld->new(); +while (1) { + $hw->print(); +} diff --git a/utils/coredump/testsources/php/gdb-dump-offsets.py b/utils/coredump/testsources/php/gdb-dump-offsets.py new file mode 100644 index 00000000..79bc1541 --- /dev/null +++ b/utils/coredump/testsources/php/gdb-dump-offsets.py @@ -0,0 +1,50 @@ +""" +gdb python script for dumping the Ruby vmStruct offsets. +""" + +def no_member_to_none(fn): + """Decorator translating errors about missing field to `None`.""" + def wrap(*args, **kwargs): + try: + return fn(*args, **kwargs) + except gdb.error as e: + if 'no member named' in str(e): + return None + raise + return wrap + +@no_member_to_none +def offset_of(ty, field, ns='struct'): + return int(gdb.parse_and_eval(f'(uintptr_t)(&(({ns} {ty}*)0)->{field})')) + +@no_member_to_none +def size_of(ty, *, ns='struct'): + return int(gdb.parse_and_eval(f'sizeof({ns} {ty})')) + + +fields = { + 'zend_executor_globals.current_execute_data': offset_of('_zend_executor_globals', 'current_execute_data'), + 'zend_executor_globals.current_execute_data': offset_of('_zend_executor_globals', 'current_execute_data'), + + 'zend_execute_data.opline': offset_of('_zend_execute_data', 'opline'), + 'zend_execute_data.function': offset_of('_zend_execute_data', 'func'), + 'zend_execute_data.this_type_info': offset_of('_zend_execute_data', 'This.u1.type_info'), + 'zend_execute_data.prev_execute_data': offset_of('_zend_execute_data', 'prev_execute_data'), + + 'zend_function.common_type': offset_of('_zend_function', 'common.type', ns='union'), + 'zend_function.common_funcname': offset_of('_zend_function', 'common.function_name', ns='union'), + 'zend_function.op_array_filename': offset_of('_zend_function', 'op_array.filename', ns='union'), + 'zend_function.op_array_linestart': offset_of('_zend_function', 'op_array.line_start', ns='union'), + 'zend_function.Sizeof': size_of('_zend_function', ns='union'), + + 'zend_string.val': offset_of('_zend_string', 'val'), + + 'zend_op.lineno': offset_of('_zend_op', 'lineno'), +} + + +for field, value in fields.items(): + if value is None: + print(f"vms.{field}: ") + else: + print(f"vms.{field}: dec={value} hex=0x{value:x}") diff --git a/utils/coredump/testsources/php/php_forever.php b/utils/coredump/testsources/php/php_forever.php new file mode 100644 index 00000000..acd3c046 --- /dev/null +++ b/utils/coredump/testsources/php/php_forever.php @@ -0,0 +1,23 @@ + + diff --git a/utils/coredump/testsources/php/prime.php b/utils/coredump/testsources/php/prime.php new file mode 100644 index 00000000..aae5fdce --- /dev/null +++ b/utils/coredump/testsources/php/prime.php @@ -0,0 +1,41 @@ +'.$i.', '; + } + } +} + +?> diff --git a/utils/coredump/testsources/python/expat.py b/utils/coredump/testsources/python/expat.py new file mode 100644 index 00000000..85581c25 --- /dev/null +++ b/utils/coredump/testsources/python/expat.py @@ -0,0 +1,28 @@ +import xml.parsers.expat + +def test(): + while True: + pass + +# 3 handler functions +def start_element(name, attrs): + print('Start element:', name, attrs) + test() +def end_element(name): + print('End element:', name) +def char_data(data): + print('Character data:', repr(data)) + +def main(): + p = xml.parsers.expat.ParserCreate() + + p.StartElementHandler = start_element + p.EndElementHandler = end_element + p.CharacterDataHandler = char_data + + p.Parse(""" +Text goes here +More text +""", 1) + +main() diff --git a/utils/coredump/testsources/python/fib.py b/utils/coredump/testsources/python/fib.py new file mode 100644 index 00000000..cf790c5e --- /dev/null +++ b/utils/coredump/testsources/python/fib.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +def recur_fibo(n): + if n <= 1: + return n + else: + return(recur_fibo(n-1) + recur_fibo(n-2)) + +while True: + print(recur_fibo(42)) diff --git a/utils/coredump/testsources/ruby/gdb-dump-offsets.py b/utils/coredump/testsources/ruby/gdb-dump-offsets.py new file mode 100644 index 00000000..49c4660a --- /dev/null +++ b/utils/coredump/testsources/ruby/gdb-dump-offsets.py @@ -0,0 +1,81 @@ +""" +gdb python script for dumping the Ruby vmStruct offsets. +""" + +def no_member_to_none(fn): + """Decorator translating errors about missing field to `None`.""" + def wrap(*args, **kwargs): + try: + return fn(*args, **kwargs) + except gdb.error as e: + if 'no member named' in str(e): + return None + raise + return wrap + +@no_member_to_none +def offset_of(ty, field): + return int(gdb.parse_and_eval(f'(uintptr_t)(&((struct {ty}*)0)->{field})')) + +@no_member_to_none +def size_of(ty, *, ns='struct'): + return int(gdb.parse_and_eval(f'sizeof({ns} {ty})')) + +@no_member_to_none +def size_of_field(ty, field): + return int(gdb.parse_and_eval(f'sizeof(((struct {ty}*)0)->{field})')) + + +fields = { + 'execution_context_struct.vm_stack': offset_of('rb_execution_context_struct', 'vm_stack'), + 'execution_context_struct.vm_stack_size': offset_of('rb_execution_context_struct', 'vm_stack_size'), + 'execution_context_struct.cfp': offset_of('rb_execution_context_struct', 'cfp'), + + 'control_frame_struct.pc': offset_of('rb_control_frame_struct', 'pc'), + 'control_frame_struct.iseq': offset_of('rb_control_frame_struct', 'iseq'), + 'control_frame_struct.ep': offset_of('rb_control_frame_struct', 'ep'), + 'control_frame_struct.size_of_control_frame_struct': size_of('rb_control_frame_struct'), + + 'iseq_struct.body': offset_of('rb_iseq_struct', 'body'), + + 'iseq_constant_body.iseq_type': offset_of('rb_iseq_constant_body', 'type'), + 'iseq_constant_body.size': offset_of('rb_iseq_constant_body', 'iseq_size'), + 'iseq_constant_body.encoded': offset_of('rb_iseq_constant_body', 'iseq_encoded'), + 'iseq_constant_body.location': offset_of('rb_iseq_constant_body', 'location'), + 'iseq_constant_body.insn_info_body': offset_of('rb_iseq_constant_body', 'insns_info.body'), + 'iseq_constant_body.insn_info_size': offset_of('rb_iseq_constant_body', 'insns_info.size'), + 'iseq_constant_body.succ_index_table': offset_of('rb_iseq_constant_body', 'insns_info.succ_index_table'), + 'iseq_constant_body.size_of_iseq_constant_body': size_of('rb_iseq_constant_body'), + + 'iseq_location_struct.pathobj': offset_of('rb_iseq_location_struct', 'pathobj'), + 'iseq_location_struct.base_label': offset_of('rb_iseq_location_struct', 'base_label'), + + 'iseq_insn_info_entry.position': offset_of('iseq_insn_info_entry', 'position'), + 'iseq_insn_info_entry.size_of_position': size_of_field('iseq_insn_info_entry', 'position'), + 'iseq_insn_info_entry.line_no': offset_of('iseq_insn_info_entry', 'line_no'), + 'iseq_insn_info_entry.size_of_line_no': size_of_field('iseq_insn_info_entry', 'line_no'), + 'iseq_insn_info_entry.size_of_iseq_insn_info_entry': size_of('iseq_insn_info_entry'), + + 'rstring_struct.as_ary': offset_of('RString', 'as.embed.ary'), + 'rstring_struct.as_heap_ptr': offset_of('RString', 'as.heap.ptr'), + + 'rarray_struct.as_ary': offset_of('RArray', 'as.ary'), + 'rarray_struct.as_heap_ptr': offset_of('RArray', 'as.heap.ptr'), + + 'succ_index_table_struct.small_block_ranks': offset_of('succ_dict_block', 'small_block_ranks'), + 'succ_index_table_struct.block_bits': offset_of('succ_dict_block', 'bits'), + 'succ_index_table_struct.succ_part': offset_of('succ_index_table', 'succ_part'), + 'succ_index_table_struct.size_of_succ_dict_block': size_of('succ_dict_block'), + 'size_of_immediate_table': size_of_field('succ_index_table', 'imm_part') * 9 // 8, + + 'size_of_value': size_of('VALUE', ns=''), + + 'rb_ractor_struct.running_ec': offset_of('rb_ractor_struct', 'threads.running_ec'), +} + + +for field, value in fields.items(): + if value is None: + print(f"vms.{field}: ") + else: + print(f"vms.{field}: dec={value} hex=0x{value:x}") diff --git a/utils/coredump/testsources/ruby/loop.rb b/utils/coredump/testsources/ruby/loop.rb new file mode 100644 index 00000000..f10cce0d --- /dev/null +++ b/utils/coredump/testsources/ruby/loop.rb @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby + +def is_prime(n) + if n < 2 + return false + elsif n == 2 + return true + end + + ((2..(Math.sqrt(n)))).each do |i| + return false if n % i == 0 + end + return true +end + +def sum_of_primes(n) + sum_of_primes = 0 + x = 2 + while x < n + if is_prime(x) + sum_of_primes += x + end + x += 1 + end + return sum_of_primes +end + +loop do + for i in 0..1000000 do + puts i, sum_of_primes(i) + end +end + diff --git a/utils/coredump/upload.go b/utils/coredump/upload.go new file mode 100644 index 00000000..a51abba8 --- /dev/null +++ b/utils/coredump/upload.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 main + +import ( + "context" + "flag" + "fmt" + + "github.com/peterbourgon/ff/v3/ffcli" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" +) + +type uploadCmd struct { + store *modulestore.Store + + // User-specified command line arguments. + path string + all bool +} + +func newUploadCmd(store *modulestore.Store) *ffcli.Command { + cmd := uploadCmd{store: store} + set := flag.NewFlagSet("upload", flag.ExitOnError) + set.StringVar(&cmd.path, "path", "", "The path to a specific test case JSON") + set.BoolVar(&cmd.all, "all", false, "Upload all referenced modules for all test cases") + return &ffcli.Command{ + Name: "upload", + ShortUsage: "upload [flags]", + ShortHelp: "Upload a test case to the remote storage", + FlagSet: set, + Exec: cmd.exec, + } +} + +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)") + } + + var paths []string + if cmd.all { + paths, err = findTestCases(false) + if err != nil { + return fmt.Errorf("failed to scan for test cases") + } + } else { + paths = []string{cmd.path} + } + + var modules []modulestore.ID //nolint:prealloc + for _, testCase := range paths { + var test *CoredumpTestCase + test, err = readTestCase(testCase) + if err != nil { + return fmt.Errorf("failed to read test case: %w", err) + } + + modules = append(modules, test.CoredumpRef) + for _, x := range test.Modules { + modules = append(modules, x.Ref) + } + } + + // We retrieve the remote module list to prevent polling the status for + // each module individually. + remoteModules, err := cmd.store.ListRemoteModules() + if err != nil { + return fmt.Errorf("failed to retrieve remote module list: %w", err) + } + + for _, id := range modules { + if _, present := remoteModules[id]; present { + continue + } + + log.Infof("Uploading module `%s`", id.String()) + if err = cmd.store.UploadModule(id); err != nil { + return fmt.Errorf("failed to upload module: %w", err) + } + } + + log.Info("All modules are present remotely") + return nil +} diff --git a/utils/errors-codegen/bpf.h.template b/utils/errors-codegen/bpf.h.template new file mode 100644 index 00000000..9a747960 --- /dev/null +++ b/utils/errors-codegen/bpf.h.template @@ -0,0 +1,14 @@ +/* WARNING: this file is auto-generated, DO NOT CHANGE MANUALLY */ + +#ifndef OPTI_ERRORS_H +#define OPTI_ERRORS_H + +typedef enum ErrorCode { +{{- range $i, $el := .Errors -}} + {{if $i}}{{printf ",\n"}}{{end}} + // {{if .Obsolete}}Deprecated: {{end}}{{.Description}} + {{enumident .Name}} = {{.ID}} +{{- end}} +} ErrorCode; + +#endif // OPTI_ERRORS_H diff --git a/utils/errors-codegen/errors.json b/utils/errors-codegen/errors.json new file mode 100644 index 00000000..c43e307f --- /dev/null +++ b/utils/errors-codegen/errors.json @@ -0,0 +1,248 @@ +[ + { + "id": 0, + "name": "ok", + "description": "Sentinel value for success: not actually an error" + }, + { + "id": 1, + "name": "unreachable", + "description": "Entered code that was believed to be unreachable" + }, + { + "id": 2, + "name": "stack_length_exceeded", + "description": "The stack trace has reached its maximum length and could not be unwound further" + }, + { + "id": 3, + "name": "empty_stack", + "description": "The trace stack was empty after unwinding completed" + }, + { + "obsolete": true, + "id": 4, + "name": "lookup_per_cpu_frame_list", + "description": "Failed to lookup entry in the per-CPU frame list" + }, + { + "id": 5, + "name": "max_tail_calls", + "description": "Maximum number of tail calls was reached" + }, + { + "id": 1000, + "name": "hotspot_no_codeblob", + "description": "Hotspot: Failure to get CodeBlob address (no heap or bad segmap)" + }, + { + "id": 1001, + "name": "hotspot_interpreter_fp", + "description": "Hotspot: Failure to unwind interpreter due to invalid FP" + }, + { + "id": 1002, + "name": "hotspot_invalid_ra", + "description": "Hotspot: Failure to unwind because return address was not found with heuristic" + }, + { + "id": 1003, + "name": "hotspot_invalid_codeblob", + "description": "Hotspot: Failure to get codeblob data or matching it to current unwind state" + }, + { + "id": 1004, + "name": "hotspot_lr_unwinding_mid_trace", + "description": "Hotspot: Unwind instructions requested LR unwinding mid-trace (nonsensical)" + }, + { + "id": 2000, + "name": "python_bad_code_object_addr", + "description": "Python: Unable to read current PyCodeObject" + }, + { + "id": 2001, + "name": "python_no_proc_info", + "description": "Python: No entry for this process exists in the Python process info array" + }, + { + "id": 2002, + "name": "python_bad_frame_object_addr", + "description": "Python: Unable to read current PyFrameObject" + }, + { + "id": 2003, + "name": "python_bad_cframe_current_frame_addr", + "description": "Python: Unable to read _PyCFrame.current_frame" + }, + { + "id": 2004, + "name": "python_read_thread_state_addr", + "description": "Python: Unable to read the thread state pointer from TLD" + }, + { + "id": 2005, + "name": "python_zero_thread_state", + "description": "Python: The thread state pointer read from TSD is zero" + }, + { + "id": 2006, + "name": "python_bad_thread_state_frame_addr", + "description": "Python: Unable to read the frame pointer from the thread state object" + }, + { + "id": 2007, + "name": "python_bad_auto_tls_key_addr", + "description": "Python: Unable to read autoTLSkey" + }, + { + "id": 2008, + "name": "python_read_tsd_base", + "description": "Python: Unable to determine the base address for thread-specific data" + }, + { + "id": 3000, + "name": "ruby_no_proc_info", + "description": "Ruby: No entry for this process exists in the Ruby process info array" + }, + { + "id": 3001, + "name": "ruby_read_stack_ptr", + "description": "Ruby: Unable to read the stack pointer from the Ruby context" + }, + { + "id": 3002, + "name": "ruby_read_stack_size", + "description": "Ruby: Unable to read the size of the VM stack from the Ruby context" + }, + { + "id": 3003, + "name": "ruby_read_cfp", + "description": "Ruby: Unable to read the control frame pointer from the Ruby context" + }, + { + "id": 3004, + "name": "ruby_read_ep", + "description": "Ruby: Unable to read the expression path from the Ruby frame" + }, + { + "id": 3005, + "name": "ruby_read_iseq_body", + "description": "Ruby: Unable to read instruction sequence body" + }, + { + "id": 3006, + "name": "ruby_read_iseq_encoded", + "description": "Ruby: Unable to read the instruction sequence encoded size" + }, + { + "id": 3007, + "name": "ruby_read_iseq_size", + "description": "Ruby: Unable to read the instruction sequence size" + }, + { + "id": 4000, + "name": "native_lookup_text_section", + "description": "Native: Unable to find the code section in the stack delta page info map" + }, + { + "id": 4001, + "name": "native_lookup_stack_delta_outer_map", + "description": "Native: Unable to look up the outer stack delta map (invalid map ID)" + }, + { + "id": 4002, + "name": "native_lookup_stack_delta_inner_map", + "description": "Native: Unable to look up the inner stack delta map (unknown text section ID)" + }, + { + "id": 4003, + "name": "native_exceeded_delta_lookup_iterations", + "description": "Native: Exceeded the maximum number of binary search steps during stack delta lookup" + }, + { + "id": 4004, + "name": "native_lookup_range", + "description": "Native: Unable to look up the stack delta from the inner map" + }, + { + "id": 4005, + "name": "native_stack_delta_invalid", + "description": "Native: The stack delta read from the delta map is marked as invalid" + }, + { + "id": 4006, + "name": "native_stack_delta_stop", + "description": "Native: The stack delta read from the delta map is a stop record" + }, + { + "id": 4007, + "name": "native_pc_read", + "description": "Native: Unable to read the next instruction pointer from memory" + }, + { + "id": 4008, + "name": "native_lr_unwinding_mid_trace", + "description": "Native: Unwind instructions requested LR unwinding mid-trace (nonsensical)" + }, + { + "id": 4009, + "name": "native_read_kernelmode_regs", + "description": "Native: Unable to read the kernel-mode registers" + }, + { + "id": 4010, + "name": "native_chase_irq_stack_link", + "description": "Native: Unable to read the IRQ stack link" + }, + { + "id": 4011, + "name": "native_unexpected_kernel_address", + "description": "Native: Unexpectedly encountered a kernel mode pointer while attempting to unwind user-mode stack" + }, + { + "id": 4012, + "name": "native_no_pid_page_mapping", + "description": "Native: Unable to locate the PID page mapping for the current instruction pointer" + }, + { + "id": 4013, + "name": "native_zero_pc", + "description": "Native: Unexpectedly encountered a instruction pointer of zero" + }, + { + "id": 4014, + "name": "native_small_pc", + "description": "Native: The instruction pointer is too small to be valid" + }, + { + "id": 4015, + "name": "native_bad_unwind_info_index", + "description": "Native: Encountered an invalid unwind_info_array index" + }, + { + "id": 4016, + "name": "native_aarch64_32bit_compat_mode", + "description": "Native: Code is running in ARM 32-bit compat mode." + }, + { + "id": 4017, + "name": "native_x64_32bit_compat_mode", + "description": "Native: Code is running in x86_64 32-bit compat mode." + }, + { + "id": 5000, + "name": "v8_bad_fp", + "description": "V8: Encountered a bad frame pointer during V8 unwinding" + }, + { + "id": 5001, + "name": "v8_bad_js_func", + "description": "V8: The JavaScript function object read from memory is invalid" + }, + { + "id": 5002, + "name": "v8_no_proc_info", + "description": "V8: No entry for this process exists in the V8 process info array" + } +] diff --git a/utils/errors-codegen/main.go b/utils/errors-codegen/main.go new file mode 100644 index 00000000..8f96d8e6 --- /dev/null +++ b/utils/errors-codegen/main.go @@ -0,0 +1,125 @@ +/* + * 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. + */ + +// errors-codegen generates the code containing the host agent error code enums. +package main + +import ( + _ "embed" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "text/template" +) + +// CodeGen describes the interface to be implemented by each code-generator. +type CodeGen interface { + Generate(out io.Writer, errors []JSONError) error +} + +var codeGens = map[string]CodeGen{ + "bpf": &BPFCodeGen{}, +} + +type JSONError struct { + ID uint64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Obsolete bool `json:"obsolete,omitempty"` +} + +//go:embed errors.json +var errorsJSON []byte + +// BPFCodeGen generates a BPF C header file. +type BPFCodeGen struct{} + +//go:embed bpf.h.template +var bpfTemplate string + +func toCEnumIdent(name string) string { + return "ERR_" + strings.ToUpper(name) +} + +func (cg *BPFCodeGen) Generate(out io.Writer, errors []JSONError) error { + tmpl := template.New("bpf-template") + + tmpl.Funcs(map[string]any{ + "enumident": toCEnumIdent, + }) + + var err error + tmpl, err = tmpl.Parse(bpfTemplate) + if err != nil { + return fmt.Errorf("failed to parse BPF C header template: %v", err) + } + + return tmpl.Execute(out, &struct { + Errors []JSONError + }{ + Errors: errors, + }) +} + +func checkUnique(errors []JSONError) error { + names := make(map[string]JSONError, len(errors)) + ids := make(map[uint64]JSONError, len(errors)) + + for _, item := range errors { + if existing, exists := names[item.Name]; exists { + return fmt.Errorf("duplicate name: %#v and %#v", existing, item) + } + if existing, exists := ids[item.ID]; exists { + return fmt.Errorf("duplicate ID: %#v and %#v", existing, item) + } + + ids[item.ID] = item + names[item.Name] = item + } + + return nil +} + +func generate(codeGenName, outputPath string) error { + var entries []JSONError + if err := json.Unmarshal(errorsJSON, &entries); err != nil { + return fmt.Errorf("failed to parse `errors.json`: %v", err) + } + + if err := checkUnique(entries); err != nil { + return err + } + + file, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create bpf.h: %v", err) + } + + cg, exists := codeGens[codeGenName] + if !exists { + return fmt.Errorf("unknown code-generator: %s", codeGenName) + } + + if err = cg.Generate(file, entries); err != nil { + return fmt.Errorf("failed to do BPF code-gen: %v", err) + } + + return nil +} + +func main() { + if len(os.Args) != 3 { + _, _ = fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + if err := generate(os.Args[1], os.Args[2]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/utils/zstpak/.gitignore b/utils/zstpak/.gitignore new file mode 100644 index 00000000..b1aaee50 --- /dev/null +++ b/utils/zstpak/.gitignore @@ -0,0 +1 @@ +zstpak diff --git a/utils/zstpak/lib/zstpak.go b/utils/zstpak/lib/zstpak.go new file mode 100644 index 00000000..55d591e0 --- /dev/null +++ b/utils/zstpak/lib/zstpak.go @@ -0,0 +1,260 @@ +/* + * 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. + */ + +// zstpak implements reading and writing for an efficiently seekable compressed file format. The +// efficient random access is achieved by compressing the data in small chunks and keeping an index +// of the chunks in a footer. The footer can then be inspected to determine in which chunk the +// required data for any offset is located. +// +// # File format +// +// >>> +// >>> for chunk in number_of_chunks: +// >>> compressed_data_offset: u64 LE # offset in compressed data +// >>> number_of_chunks: u64 LE +// >>> decompressed_size: u64 LE +// >>> chunk_size: u64 LE +// >>> magic: [8]char +// +// Using relative offsets and variable size ints in the footer could shave off a few more bytes, +// but was omitted for simplicity. + +package zstpak + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "os" + + "github.com/DataDog/zstd" +) + +// footerSize is the size of the static portion of the footer (without the index data). +const footerSize = 32 + +// magic defines the file magic that uniquely identifies zstpak files. +const magic = "ZSTPAK00" + +// footer contains the meta-information stored at the end of zstpak files. +type footer struct { + chunkSize uint64 + uncompressedSize uint64 + index []uint64 +} + +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") + } + 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)") + } + + chunkSize := binary.LittleEndian.Uint64(buf[16:]) + uncompressedSize := binary.LittleEndian.Uint64(buf[8:]) + numberOfChunks := binary.LittleEndian.Uint64(buf[0:]) + + // Read raw index from file. + if fileSize < footerSize+numberOfChunks*8 { + return nil, fmt.Errorf("file too small to hold index table") + } + rawIndex := make([]byte, numberOfChunks*8) + indexOffset := fileSize - footerSize - numberOfChunks*8 + if _, err := input.ReadAt(rawIndex, int64(indexOffset)); err != nil { + return nil, fmt.Errorf("failed to read index from file: %w", err) + } + + // Convert into array of uint64. + index := make([]uint64, 0, numberOfChunks) + 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") + } + index = append(index, entry) + } + + return &footer{ + chunkSize: chunkSize, + uncompressedSize: uncompressedSize, + index: index, + }, nil +} + +func (ftr *footer) write(out io.Writer) error { + for _, offset := range ftr.index { + if err := binary.Write(out, binary.LittleEndian, offset); err != nil { + return fmt.Errorf("failed to write index entry: %w", err) + } + } + + if err := binary.Write(out, binary.LittleEndian, uint64(len(ftr.index))); err != nil { + return fmt.Errorf("failed to write number of entries: %w", err) + } + if err := binary.Write(out, binary.LittleEndian, ftr.uncompressedSize); err != nil { + return fmt.Errorf("failed to write uncompressed size: %w", err) + } + if err := binary.Write(out, binary.LittleEndian, ftr.chunkSize); err != nil { + return fmt.Errorf("failed to write chunk size: %w", err) + } + if _, err := out.Write([]byte(magic)); err != nil { + return fmt.Errorf("failed to write magic: %w", err) + } + + return nil +} + +// CompressInto reads data from an input reader, writing it out in compressed form. The chunk size +// determines how often to create new chunks. Higher numbers increase compression rates, but come +// at the cost of making random access less efficient. +func CompressInto(in io.Reader, out io.Writer, chunkSize uint64) error { + readBuf := make([]byte, chunkSize) + compressBuf := make([]byte, chunkSize) + + // Compress chunks, memorizing their start offsets. + index := []uint64{0} + writeOffset := uint64(0) + uncompressedSize := uint64(0) + for { + n, err := io.ReadFull(in, readBuf) + if err != nil { + if err == io.EOF { + break + } else if err == io.ErrUnexpectedEOF { + // Last chunk: truncate our buffer and continue. Next read will + // return EOF and thus break the loop. + readBuf = readBuf[:n] + } else { + return err + } + } + + compressed, err := zstd.Compress(compressBuf, readBuf) + if err != nil { + return fmt.Errorf("failed to compress buffer: %w", err) + } + + uncompressedSize += uint64(n) + writeOffset += uint64(len(compressed)) + index = append(index, writeOffset) + + if _, err = out.Write(compressed); err != nil { + return fmt.Errorf("failed to write compressed data: %w", err) + } + } + + // Write footer. + ftr := footer{ + uncompressedSize: uncompressedSize, + chunkSize: chunkSize, + index: index, + } + return ftr.write(out) +} + +// Reader allows random access reads within zstpak files. Created via the `Open` method. +type Reader struct { + file *os.File + footer *footer +} + +// Open a zstpak file for random access reading. +func Open(path string) (*Reader, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + + fileInfo, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("failed to stat file: %w", err) + } + + hdr, err := readFooter(file, uint64(fileInfo.Size())) + if err != nil { + return nil, err + } + + return &Reader{ + file: file, + footer: hdr, + }, nil +} + +// UncompressedSize returns the size of the packed file if it was fully decompressed. +func (reader *Reader) UncompressedSize() uint64 { + return reader.footer.uncompressedSize +} + +// ChunkSize returns the size of the compressed chunks in this file. +func (reader *Reader) ChunkSize() uint64 { + return reader.footer.chunkSize +} + +// Close implements the `Closer` interface. +func (reader *Reader) Close() error { + return reader.file.Close() +} + +// ReadAt implements the `ReaderAt` interface. +func (reader *Reader) ReadAt(p []byte, off int64) (n int, err error) { + writeOffset := 0 + remaining := len(p) + chunkIdx := int(off) / int(reader.footer.chunkSize) + skipOffset := int(off) % int(reader.footer.chunkSize) + + for remaining > 0 { + if chunkIdx+1 >= len(reader.footer.index) { + return writeOffset, io.EOF + } + + // Read compressed chunk from disk. + compressedChunkStart := reader.footer.index[chunkIdx] + compressedChunkLen := reader.footer.index[chunkIdx+1] - compressedChunkStart + decompressed, err := reader.getDecompressedChunk(compressedChunkStart, compressedChunkLen) + if err != nil { + return writeOffset, err + } + + // Copy data to output buffer. + if skipOffset > len(decompressed) { + return 0, fmt.Errorf("corrupted chunk data") + } + copyLen := min(remaining, len(decompressed)-skipOffset) + copy(p[writeOffset:][:copyLen], decompressed[skipOffset:][:copyLen]) + + // Only apply skipping in first iteration. + skipOffset = 0 + + // Adjust offset and capacity. + writeOffset += copyLen + remaining -= copyLen + chunkIdx++ + } + + return writeOffset, nil +} + +func (reader *Reader) getDecompressedChunk(start, length uint64) ([]byte, error) { + compressedChunk := make([]byte, length) + if _, err := reader.file.ReadAt(compressedChunk, int64(start)); err != nil { + return nil, fmt.Errorf("failed to read chunk data: %w", err) + } + + decompressed, err := zstd.Decompress(nil, compressedChunk) + if err != nil { + return nil, fmt.Errorf("failed to decompress chunk: %w", err) + } + + return decompressed, nil +} diff --git a/utils/zstpak/main.go b/utils/zstpak/main.go new file mode 100644 index 00000000..2d0553d9 --- /dev/null +++ b/utils/zstpak/main.go @@ -0,0 +1,79 @@ +/* + * 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. + */ + +// Implements a command-line utility for compressing and decompressing zstpak files. + +package main + +import ( + "flag" + "fmt" + "os" + + zstpak "github.com/elastic/otel-profiling-agent/utils/zstpak/lib" +) + +func tryMain() error { + var compress, decompress bool + var in, out string + var chunkSize uint64 + + flag.BoolVar(&compress, "c", false, "Compress data into zstpak format") + flag.BoolVar(&decompress, "d", false, "Decompress data from zstpak format") + flag.StringVar(&in, "i", "", "The input file path") + flag.StringVar(&out, "o", "", "The output file path") + flag.Uint64Var(&chunkSize, "chunk-size", 65536, "The chunk size to use") + flag.Parse() + + if compress == decompress { + return fmt.Errorf("must specify either `-c` or `-d`") + } + if in == "" { + return fmt.Errorf("missing required argument `i`") + } + if out == "" { + return fmt.Errorf("missing required argument `o`") + } + + outputFile, err := os.Create(out) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + + switch { + case compress: + inputFile, err := os.Open(in) + if err != nil { + return fmt.Errorf("failed to open input file: %w", err) + } + + if err = zstpak.CompressInto(inputFile, outputFile, chunkSize); err != nil { + return fmt.Errorf("failed to compress file: %w", err) + } + case decompress: + pak, err := zstpak.Open(in) + if err != nil { + return fmt.Errorf("failed to open zstpak file: %w", err) + } + + buf := make([]byte, pak.UncompressedSize()) + if _, err = pak.ReadAt(buf, 0); err != nil { + return fmt.Errorf("failed to read zstpak: %w", err) + } + + if _, err = outputFile.Write(buf); err != nil { + return fmt.Errorf("failed to write to output file: %w", err) + } + } + + return nil +} + +func main() { + if err := tryMain(); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + } +}