├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── include ├── LICENSE.BSD-2-Clause ├── arm64_compat.h ├── bpf_core_read.h ├── bpf_endian.h ├── bpf_helper_defs.h ├── bpf_helpers.h ├── bpf_tracing.h ├── update.sh └── vmlinux.h ├── main.go ├── oomprof.c ├── oomprof ├── bpf_arm64_bpfel.go ├── bpf_arm64_bpfel.o ├── bpf_x86_bpfel.go ├── bpf_x86_bpfel.o ├── elfreader.go ├── log.go ├── monitor.go ├── monitor_stub.go ├── oom_channel_test.go ├── oomprof_test.go ├── pprof.go ├── reporter.go ├── scan.go └── trace.go └── tests ├── compile-oom ├── go.mod └── main.go ├── deepstack ├── go.mod └── main.go ├── gccache └── main.go ├── oomer └── main.go ├── parca-agent-test.sh ├── run-all-memlimited.sh ├── run-in-cgroup.sh └── run-tests.sh /.gitignore: -------------------------------------------------------------------------------- 1 | ci-kernels* 2 | tests/*.taux 3 | *.pb.gz 4 | oompa 5 | tests/oomprof.test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity granting the License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all 15 | other entities that control, are controlled by, or are under common 16 | control with that entity. For the purposes of this definition, 17 | "control" means (i) the power, direct or indirect, to cause the 18 | direction or management of such entity, whether by contract or 19 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity 23 | exercising permissions granted by this License. 24 | 25 | "Source" shall mean the preferred form for making modifications, 26 | including but not limited to software source code, documentation 27 | source, and configuration files. 28 | 29 | "Object" shall mean any form resulting from mechanical 30 | transformation or translation of a Source form, including but 31 | not limited to compiled object code, generated documentation, 32 | and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or 35 | Object form, made available under the License, as indicated by a 36 | copyright notice that is included in or attached to the work 37 | (which shall not include combinations of the Work with other works). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object 40 | form, that is based upon (or derived from) the Work and for which the 41 | editorial revisions, annotations, elaborations, or other modifications 42 | represent, as a whole, an original work of authorship. For the purposes 43 | of this License, Derivative Works shall not include works that remain 44 | separable from, or merely link (or bind by name) to the interfaces of, 45 | the Work and derivative works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including 48 | the original version of the Work and any modifications or additions 49 | to that Work or Derivative Works thereof, that is intentionally 50 | submitted to Licensor for inclusion in the Work by the copyright owner 51 | or by an individual or Legal Entity authorized to submit on behalf of 52 | the copyright owner. For the purposes of this definition, "submitted" 53 | means any form of electronic, verbal, or written communication sent 54 | to the Licensor or its representatives, including but not limited to 55 | communication on electronic mailing lists, source code control 56 | systems, and issue tracking systems that are managed by, or on behalf 57 | of, the Licensor for the purpose of discussing and improving the Work, 58 | but excluding communication that is conspicuously marked or otherwise 59 | designated in writing by the copyright owner as "Not a Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity 62 | on behalf of whom a Contribution has been received by Licensor and 63 | subsequently incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of 66 | this License, each Contributor hereby grants to You a perpetual, 67 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 68 | copyright license to use, reproduce, modify, distribute, prepare 69 | Derivative Works of, and publicly display the Work and such Derivative 70 | Works in all media and formats whether now known or hereafter devised. 71 | 72 | 3. Grant of Patent License. Subject to the terms and conditions of 73 | this License, each Contributor hereby grants to You a perpetual, 74 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 75 | (except as stated in this section) patent license to make, have made, 76 | use, offer to sell, sell, import, and otherwise transfer the Work, 77 | where such license applies only to those patent claims licensable 78 | by such Contributor that are necessarily infringed by their 79 | Contribution(s) alone or by combination of their Contribution(s) 80 | with the Work to which such Contribution(s) was submitted. If You 81 | institute patent litigation against any entity (including a 82 | cross-claim or counterclaim in a lawsuit) alleging that the Work 83 | or a Contribution incorporated within the Work constitutes direct 84 | or contributory patent infringement, then any patent licenses 85 | granted to You under this License for that Work shall terminate 86 | as of the date such litigation is filed. 87 | 88 | 4. Redistribution. You may reproduce and distribute copies of the 89 | Work or Derivative Works thereof in any medium, with or without 90 | modifications, and in Source or Object form, provided that You 91 | meet the following conditions: 92 | 93 | (a) You must give any other recipients of the Work or 94 | Derivative Works a copy of this License; and 95 | 96 | (b) You must cause any modified files to carry prominent notices 97 | stating that You changed the files; and 98 | 99 | (c) You must retain, in the Source form of any Derivative Works 100 | that You distribute, all copyright, patent, trademark, and 101 | attribution notices from the Source form of the Work, 102 | excluding those notices that do not pertain to any part of 103 | the Derivative Works; and 104 | 105 | (d) If the Work includes a "NOTICE" text file as part of its 106 | distribution, then any Derivative Works that You distribute must 107 | include a readable copy of the attribution notices contained 108 | within such NOTICE file, excluding those notices that do not 109 | pertain to any part of the Derivative Works, in at least one 110 | of the following places: within a NOTICE text file distributed 111 | as part of the Derivative Works; within the Source form or 112 | documentation, if provided along with the Derivative Works; or, 113 | within a display generated by the Derivative Works, if and 114 | wherever such third-party notices normally appear. The contents 115 | of the NOTICE file are for informational purposes only and 116 | do not modify the License. You may add Your own attribution 117 | notices within Derivative Works that You distribute, alongside 118 | or as an addendum to the NOTICE text from the Work, provided 119 | that such additional attribution notices cannot be construed 120 | as modifying the License. 121 | 122 | You may add Your own copyright notice to Your modifications and 123 | may provide additional or different license terms and conditions 124 | for use, reproduction, or distribution of Your modifications, or 125 | for any such Derivative Works as a whole, provided Your use, 126 | reproduction, and distribution of the Work otherwise complies with 127 | the conditions stated in this License. 128 | 129 | 5. Submission of Contributions. Unless You explicitly state otherwise, 130 | any Contribution intentionally submitted for inclusion in the Work 131 | by You to the Licensor shall be under the terms and conditions of 132 | this License, without any additional terms or conditions. 133 | Notwithstanding the above, nothing herein shall supersede or modify 134 | the terms of any separate license agreement you may have executed 135 | with Licensor regarding such Contributions. 136 | 137 | 6. Trademarks. This License does not grant permission to use the trade 138 | names, trademarks, service marks, or product names of the Licensor, 139 | except as required for reasonable and customary use in describing the 140 | origin of the Work and reproducing the content of the NOTICE file. 141 | 142 | 7. Disclaimer of Warranty. Unless required by applicable law or 143 | agreed to in writing, Licensor provides the Work (and each 144 | Contributor provides its Contributions) on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 146 | implied, including, without limitation, any warranties or conditions 147 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 148 | PARTICULAR PURPOSE. You are solely responsible for determining the 149 | appropriateness of using or redistributing the Work and assume any 150 | risks associated with Your exercise of permissions under this License. 151 | 152 | 8. Limitation of Liability. In no event and under no legal theory, 153 | whether in tort (including negligence), contract, or otherwise, 154 | unless required by applicable law (such as deliberate and grossly 155 | negligent acts) or agreed to in writing, shall any Contributor be 156 | liable to You for damages, including any direct, indirect, special, 157 | incidental, or consequential damages of any character arising as a 158 | result of this License or out of the use or inability to use the 159 | Work (including but not limited to damages for loss of goodwill, 160 | work stoppage, computer failure or malfunction, or any and all 161 | other commercial damages or losses), even if such Contributor 162 | has been advised of the possibility of such damages. 163 | 164 | 9. Accepting Warranty or Support. You are not required to accept 165 | warranty or support for the Work from any Contributor. However, 166 | you may choose to offer and charge a fee for warranty, support, 167 | indemnity or other liability obligations consistent with this 168 | License. When accepting any such obligations on your own behalf 169 | or on behalf of another Contributor, you must obtain the 170 | Contributor's express prior written consent and acknowledge 171 | that the Contributor may be liable to You for any damages 172 | incurred by You as a result of accepting such warranty or support. 173 | 174 | END OF TERMS AND CONDITIONS 175 | 176 | APPENDIX: How to apply the Apache License to your work. 177 | 178 | To apply the Apache License to your work, attach the following 179 | boilerplate notice, with the fields enclosed by brackets "[]" 180 | replaced with your own identifying information. Don't include 181 | the brackets! The text should be enclosed in the appropriate 182 | comment syntax for the file format. We also recommend that a 183 | file or class name and description of purpose be included on the 184 | same "license" line as the copyright notice for easier 185 | identification within third-party archives. 186 | 187 | Copyright [yyyy] [name of copyright owner] 188 | 189 | Licensed under the Apache License, Version 2.0 (the "License"); 190 | you may not use this file except in compliance with the License. 191 | You may obtain a copy of the License at 192 | 193 | http://www.apache.org/licenses/LICENSE-2.0 194 | 195 | Unless required by applicable law or agreed to in writing, software 196 | distributed under the License is distributed on an "AS IS" BASIS, 197 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 198 | See the License for the specific language governing permissions and 199 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | .PHONY: all oompa tests tests-arm64 clean lint generate kernel-tests kernel-tests-quick 4 | 5 | AMD64_KERNELS = 5.4.276 5.10.217 5.15.159 6.1.91 6.6.31 6.8.10 6.9.1 6.12.16 6 | ARM64_KERNELS = 6.6.31 6.8.4 6.9.1 6.12.16 6.13.4 7 | 8 | GO_TAGS = osusergo,netgo,static_build 9 | GO_ARCH ?= $(shell uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g') 10 | export GOARCH = $(GO_ARCH) 11 | 12 | lint: 13 | go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run 14 | 15 | clean: 16 | rm -f oompa *.taux tests/*.taux tests/*.test oomprof/bpf_*.* 17 | 18 | generate: 19 | go generate ./oomprof 20 | 21 | oompa: generate 22 | go build -tags $(GO_TAGS) -ldflags='-extldflags=-static' -o oompa . 23 | 24 | tests: generate 25 | go test -tags $(GO_TAGS) -ldflags='-extldflags=-static' -c -o ./tests/oomprof.test ./oomprof 26 | go build -tags $(GO_TAGS) -ldflags='-extldflags=-static' -o ./tests/oomer.taux ./tests/oomer 27 | go build -tags $(GO_TAGS) -ldflags='-extldflags=-static' -o ./tests/gccache.taux ./tests/gccache 28 | 29 | cgroup-tests: tests 30 | cd tests && sudo ./oomprof.test -test.timeout 5m -test.v -test.run TestOOMProf 2>&1 | tee oomprof.log 31 | 32 | tests-arm64: 33 | GOARCH=arm64 go generate ./oomprof 34 | GOARCH=arm64 go test -tags $(GO_TAGS) -ldflags='-extldflags=-static' -c -o ./tests/oomprof.test ./oomprof 35 | GOARCH=arm64 go build -tags $(GO_TAGS) -ldflags='-extldflags=-static' -o ./tests/oomer.taux ./tests/oomer 36 | GOARCH=arm64 go build -tags $(GO_TAGS) -ldflags='-extldflags=-static' -o ./tests/gccache.taux ./tests/gccache 37 | GOARCH=arm64 go build -tags $(GO_TAGS) -ldflags='-extldflags=-static' -o ./tests/compile-oom.taux ./tests/compile-oom 38 | 39 | all: oompa tests 40 | 41 | arm64-kernels: 42 | @DEST=ci-kernels-arm64; \ 43 | mkdir -p $$DEST; \ 44 | for kernel in $(ARM64_KERNELS); do \ 45 | if [ -f $$DEST/$$kernel/boot/vmlinuz ]; then \ 46 | echo "ARM64 kernel $$kernel already exists, skipping..."; \ 47 | else \ 48 | echo "Downloading ARM64 kernel $$kernel..."; \ 49 | echo "FROM ghcr.io/cilium/ci-kernels:$$kernel" | docker buildx build --platform linux/arm64 --quiet --pull --output="$$DEST" -; \ 50 | mkdir -p $$DEST/$$kernel; \ 51 | mv $$DEST/boot/vmlinuz $$DEST/$$kernel/; \ 52 | fi \ 53 | done 54 | 55 | amd64-kernels: 56 | @DEST=ci-kernels-amd64; \ 57 | mkdir -p $$DEST; \ 58 | for kernel in $(AMD64_KERNELS); do \ 59 | if [ -f $$DEST/$$kernel/boot/vmlinuz ]; then \ 60 | echo "AMD64 kernel $$kernel already exists, skipping..."; \ 61 | else \ 62 | echo "Downloading AMD64 kernel $$kernel..."; \ 63 | echo "FROM ghcr.io/cilium/ci-kernels:$$kernel" | docker buildx build --platform linux/amd64 --quiet --pull --output="$$DEST" -; \ 64 | mkdir -p $$DEST/$$kernel; \ 65 | mv $$DEST/boot/vmlinuz $$DEST/$$kernel/; \ 66 | fi \ 67 | done 68 | 69 | kernel-tests: amd64-kernels arm64-kernels 70 | for kernel in $(AMD64_KERNELS); do \ 71 | cd tests && KERN_DIR=../ci-kernels-amd64 ./run-tests.sh $$kernel; \ 72 | done 73 | for kernel in $(ARM64_KERNELS); do \ 74 | cd tests && QEMU_ARCH=aarch64 KERN_DIR=../ci-kernels-arm64 ./run-tests.sh $$kernel; \ 75 | done 76 | 77 | # Quick test with just a couple of kernels 78 | kernel-tests-quick: tests-arm64 79 | cd tests && QEMU_ARCH=aarch64 KERN_DIR=../ci-kernels-arm64 ./run-tests.sh 6.13.4 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OOMProf - eBPF-based OOM Kill Profiler 2 | 3 | OOMProf is an eBPF-based process monitor that automatically captures heap profiles from Go programs just before they are killed by the Linux Out-of-Memory (OOM) killer, or on-demand for specific processes. This enables post-mortem analysis of memory usage patterns that led to OOM conditions. 4 | 5 | ## Features 6 | 7 | - **Real-time Go process detection**: Automatically discovers and monitors running Go programs 8 | - **Pre-OOM profiling**: Captures memory profiles at the moment of OOM kill signals 9 | - **On-demand profiling**: Profile specific processes by PID using the `-p` flag 10 | - **eBPF-powered monitoring**: Uses kernel tracepoints for low-overhead and stable process monitoring 11 | - **Standard pprof output**: Generates profiles compatible with Go's pprof toolchain 12 | - **Memory limit testing**: Includes cgroup-based testing framework for reproducible OOM scenarios 13 | 14 | ## How It Works 15 | 16 | OOMProf uses eBPF tracepoints to monitor kernel OOM killer events and automatically: 17 | 18 | 1. **Scans** the system for running Go processes and tracks their memory bucket addresses 19 | 2. **Detects** when the OOM killer is about to terminate a Go process via `oom/mark_victim` tracepoint 20 | 3. **Captures** the process's heap profile data from kernel space using `signal/signal_deliver` tracepoint 21 | 4. **Generates** a standard pprof-compatible profile file 22 | 23 | The captured profiles show memory allocation patterns, call stacks, and heap usage that can help identify memory leaks, excessive allocations, or other issues that led to the OOM condition. 24 | 25 | ## Requirements 26 | 27 | - Linux kernel with eBPF support (4.9+) 28 | - Root privileges (required for eBPF program loading) 29 | - Go 1.21+ for building 30 | 31 | ## Installation 32 | 33 | ```bash 34 | # Clone the repository 35 | git clone https://github.com/parca-dev/oomprof.git 36 | cd oomprof 37 | 38 | # Build the binaries 39 | make 40 | ``` 41 | 42 | This creates: 43 | - `oompa` - Main OOMProf monitor (OOM Profiler Agent) 44 | - `tests/oomer.taux` - Test binary for generating OOM conditions 45 | - `tests/gccache.taux` - Test binary for GC cache stress testing 46 | - `oomprof.test` - Test suite 47 | 48 | ## Usage 49 | 50 | ### Basic Monitoring 51 | 52 | Run OOMProf as root to monitor all Go processes: 53 | 54 | ```bash 55 | sudo ./oompa 56 | ``` 57 | 58 | When a Go process is OOM killed, `oompa` will automatically generate a profile file named `{command}-{pid}.pb.gz`. 59 | 60 | ### On-Demand Profiling 61 | 62 | Profile specific processes by PID using the `-p` flag with comma-delimited PIDs: 63 | 64 | ```bash 65 | # Profile a single process 66 | sudo ./oompa -p 1234 67 | 68 | # Profile multiple processes 69 | sudo ./oompa -p 1234,5678,9012 70 | ``` 71 | 72 | This will generate profile files for each specified PID and exit once profiling is complete. 73 | 74 | ### Analyzing Profiles 75 | 76 | View the captured profile using Go's pprof tool: 77 | 78 | ```bash 79 | # Interactive analysis 80 | go tool pprof {command}-{pid}.pb.gz 81 | 82 | # Generate a web interface 83 | go tool pprof -http=:8080 {command}-{pid}.pb.gz 84 | 85 | # View top memory consumers 86 | go tool pprof -top {command}-{pid}.pb.gz 87 | ``` 88 | 89 | ### Programmatic Usage 90 | 91 | OOMProf can be used as a library in your own applications: 92 | 93 | ```go 94 | package main 95 | 96 | import ( 97 | "context" 98 | "fmt" 99 | "log" 100 | "os" 101 | "time" 102 | 103 | "github.com/parca-dev/oomprof/oomprof" 104 | ) 105 | 106 | func main() { 107 | profileChan := make(chan oomprof.ProfileData) 108 | state, err := oomprof.Setup(context.Background(), &oomprof.Config{}, profileChan) 109 | if err != nil { 110 | fmt.Fprintf(os.Stderr, "Failed to setup OOM profiler: %v\n", err) 111 | os.Exit(1) 112 | } 113 | defer state.Close() 114 | 115 | for profile := range profileChan { 116 | // Create filename with timestamp: command-pid-YYYYMMDDHHmmss.pb.gz 117 | timestamp := time.Now().Format("20060102150405") 118 | filename := fmt.Sprintf("%s-%d-%s.pb.gz", profile.Command, profile.PID, timestamp) 119 | 120 | f, err := os.Create(filename) 121 | if err != nil { 122 | log.Printf("Failed to create profile file %s: %v\n", filename, err) 123 | continue 124 | } 125 | if err := profile.Profile.Write(f); err != nil { 126 | log.Printf("Failed to write profile %s: %v\n", filename, err) 127 | } 128 | f.Close() 129 | log.Printf("Saved profile for %s (PID %d) to %s\n", profile.Command, profile.PID, filename) 130 | } 131 | } 132 | ``` 133 | 134 | ### Testing with Controlled OOM 135 | 136 | Test OOMProf with the included test binaries: 137 | 138 | ```bash 139 | # Run tests with different memory limits 140 | sudo go test -v ./oomprof -run TestOOMProf 141 | 142 | # Test with specific memory constraints 143 | sudo go test -v ./oomprof -run TestOOMProfLowMemoryLimits 144 | ``` 145 | 146 | ## Configuration 147 | 148 | ### Environment Variables 149 | 150 | - `GODEBUG=memprofilerate=1` - Enable detailed memory profiling (set automatically by test programs) 151 | 152 | 153 | ## Architecture 154 | 155 | ``` 156 | ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ 157 | │ Go Process │ │ eBPF │ │ OOMProf │ 158 | │ │ │ │ │ │ 159 | │ ┌─────────────┐ │ │ ┌──────────────┐ │ │ ┌─────────────┐ │ 160 | │ │ Memory │ │ │ │ oom/mark_ │ │ │ │ Process │ │ 161 | │ │ Allocations │ │────┼▶│ victim │ │────┼▶│ Scanner │ │ 162 | │ │ │ │ │ │ signal/ │ │ │ │ │ │ 163 | │ └─────────────┘ │ │ │ signal_ │ │ │ └─────────────┘ │ 164 | │ │ │ │ deliver │ │ │ │ 165 | │ ┌─────────────┐ │ │ └──────────────┘ │ │ ┌─────────────┐ │ 166 | │ │ runtime. │ │ │ ┌──────────────┐ │ │ │ Profile │ │ 167 | │ │ mbuckets │ │◀───┼─┤ record_ │ ├────┼▶│ Generator │ │ 168 | │ └─────────────┘ │ │ │ profile_ │ │ │ └─────────────┘ │ 169 | │ │ │ │ buckets │ │ │ │ 170 | └─────────────────┘ │ └──────────────┘ │ └─────────────────┘ 171 | └──────────────────┘ 172 | ``` 173 | 174 | ## Development 175 | 176 | ### Building 177 | 178 | ```bash 179 | # Generate eBPF objects and build binaries 180 | make 181 | 182 | # Run tests (requires root) 183 | sudo go test -v ./oomprof 184 | 185 | # Run specific test with memory limits 186 | sudo go test -v ./oomprof -run TestOOMProfLowMemoryLimits 187 | ``` 188 | 189 | ### Project Structure 190 | 191 | ``` 192 | oomprof/ 193 | ├── main.go # CLI interface 194 | ├── oomprof.c # eBPF programs (kernel space) 195 | ├── oomprof/ 196 | │ ├── monitor.go # Main monitoring logic 197 | │ ├── pprof.go # Profile generation 198 | │ └── oomprof_test.go # Test suite 199 | ├── tests/ 200 | │ ├── oomer/ # Memory allocation test scenarios 201 | │ ├── gccache/ # GC cache stress test program 202 | │ └── compile-oom/ # Go compiler stress test program 203 | └── include/ # eBPF headers 204 | ``` 205 | 206 | ## Roadmap 207 | 208 | ### Current Features (MVP) 209 | - [x] eBPF-based OOM monitoring 210 | - [x] Go process heap profiling 211 | - [x] Standard pprof output format 212 | - [x] Cgroup-based testing framework 213 | 214 | ### Planned Features 215 | - [ ] Goroutine dump collection 216 | - [ ] Kubernetes/cgroup deployment support 217 | - [ ] Remote profile upload (pprof.me integration) 218 | - [ ] Integration with standard observability reporters 219 | - [ ] jemalloc/tcmalloc/mimalloc support 220 | - [ ] Python memory profiling 221 | 222 | ## Contributing 223 | 224 | Contributions are welcome! Please see the [development](#development) section for build instructions and testing guidelines. 225 | 226 | ## License 227 | 228 | This project is licensed under the Apache License 2.0. See LICENSE for details. 229 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/parca-dev/oomprof 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/KimMachineGun/automemlimit v0.7.3 7 | github.com/cilium/ebpf v0.19.0 8 | github.com/containerd/cgroups/v3 v3.0.5 9 | github.com/elastic/go-freelru v0.16.0 10 | github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a 11 | github.com/sirupsen/logrus v1.9.3 12 | github.com/stretchr/testify v1.11.1 13 | ) 14 | 15 | require ( 16 | github.com/containerd/log v0.1.0 // indirect 17 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/godbus/dbus/v5 v5.1.0 // indirect 20 | github.com/google/go-cmp v0.7.0 // indirect 21 | github.com/opencontainers/runtime-spec v1.2.0 // indirect 22 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/rogpeppe/go-internal v1.13.1 // indirect 25 | golang.org/x/sys v0.35.0 // indirect 26 | google.golang.org/protobuf v1.36.8 // indirect 27 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KimMachineGun/automemlimit v0.7.3 h1:oPgMp0bsWez+4fvgSa11Rd9nUDrd8RLtDjBoT3ro+/A= 2 | github.com/KimMachineGun/automemlimit v0.7.3/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 3 | github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao= 4 | github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY= 5 | github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= 6 | github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= 7 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 8 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 9 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 10 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/elastic/go-freelru v0.16.0 h1:gG2HJ1WXN2tNl5/p40JS/l59HjvjRhjyAa+oFTRArYs= 15 | github.com/elastic/go-freelru v0.16.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I= 16 | github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= 17 | github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= 18 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 19 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 20 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 21 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 22 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 23 | github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= 24 | github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= 25 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 26 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 27 | github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= 28 | github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= 29 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 30 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 31 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 32 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 33 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 34 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 35 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 36 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 37 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 38 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 39 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 40 | github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= 41 | github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 42 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 43 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 47 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 48 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 49 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 52 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 53 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 54 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 55 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 56 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 57 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 58 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 59 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 62 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 63 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 64 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 67 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 68 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 69 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 70 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 71 | -------------------------------------------------------------------------------- /include/LICENSE.BSD-2-Clause: -------------------------------------------------------------------------------- 1 | Valid-License-Identifier: BSD-2-Clause 2 | SPDX-URL: https://spdx.org/licenses/BSD-2-Clause.html 3 | Usage-Guide: 4 | To use the BSD 2-clause "Simplified" License put the following SPDX 5 | tag/value pair into a comment according to the placement guidelines in 6 | the licensing rules documentation: 7 | SPDX-License-Identifier: BSD-2-Clause 8 | License-Text: 9 | 10 | Copyright (c) . All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | 1. Redistributions of source code must retain the above copyright notice, 16 | this list of conditions and the following disclaimer. 17 | 18 | 2. Redistributions in binary form must reproduce the above copyright 19 | notice, this list of conditions and the following disclaimer in the 20 | documentation and/or other materials provided with the distribution. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 26 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 27 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 28 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 29 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 30 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /include/arm64_compat.h: -------------------------------------------------------------------------------- 1 | // ARM64 compatibility definitions for cross-compilation 2 | #ifndef __ARM64_COMPAT_H__ 3 | #define __ARM64_COMPAT_H__ 4 | 5 | #if defined(__aarch64__) || defined(__TARGET_ARCH_arm64) 6 | 7 | // ARM64 user_pt_regs structure 8 | // This matches the kernel's definition for ARM64 9 | struct user_pt_regs { 10 | __u64 regs[31]; 11 | __u64 sp; 12 | __u64 pc; 13 | __u64 pstate; 14 | }; 15 | 16 | #endif // __aarch64__ || __TARGET_ARCH_arm64 17 | 18 | #endif // __ARM64_COMPAT_H__ -------------------------------------------------------------------------------- /include/bpf_core_read.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | #ifndef __BPF_CORE_READ_H__ 3 | #define __BPF_CORE_READ_H__ 4 | 5 | /* 6 | * enum bpf_field_info_kind is passed as a second argument into 7 | * __builtin_preserve_field_info() built-in to get a specific aspect of 8 | * a field, captured as a first argument. __builtin_preserve_field_info(field, 9 | * info_kind) returns __u32 integer and produces BTF field relocation, which 10 | * is understood and processed by libbpf during BPF object loading. See 11 | * selftests/bpf for examples. 12 | */ 13 | enum bpf_field_info_kind { 14 | BPF_FIELD_BYTE_OFFSET = 0, /* field byte offset */ 15 | BPF_FIELD_BYTE_SIZE = 1, 16 | BPF_FIELD_EXISTS = 2, /* field existence in target kernel */ 17 | BPF_FIELD_SIGNED = 3, 18 | BPF_FIELD_LSHIFT_U64 = 4, 19 | BPF_FIELD_RSHIFT_U64 = 5, 20 | }; 21 | 22 | /* second argument to __builtin_btf_type_id() built-in */ 23 | enum bpf_type_id_kind { 24 | BPF_TYPE_ID_LOCAL = 0, /* BTF type ID in local program */ 25 | BPF_TYPE_ID_TARGET = 1, /* BTF type ID in target kernel */ 26 | }; 27 | 28 | /* second argument to __builtin_preserve_type_info() built-in */ 29 | enum bpf_type_info_kind { 30 | BPF_TYPE_EXISTS = 0, /* type existence in target kernel */ 31 | BPF_TYPE_SIZE = 1, /* type size in target kernel */ 32 | BPF_TYPE_MATCHES = 2, /* type match in target kernel */ 33 | }; 34 | 35 | /* second argument to __builtin_preserve_enum_value() built-in */ 36 | enum bpf_enum_value_kind { 37 | BPF_ENUMVAL_EXISTS = 0, /* enum value existence in kernel */ 38 | BPF_ENUMVAL_VALUE = 1, /* enum value value relocation */ 39 | }; 40 | 41 | #define __CORE_RELO(src, field, info) \ 42 | __builtin_preserve_field_info((src)->field, BPF_FIELD_##info) 43 | 44 | #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 45 | #define __CORE_BITFIELD_PROBE_READ(dst, src, fld) \ 46 | bpf_probe_read_kernel( \ 47 | (void *)dst, \ 48 | __CORE_RELO(src, fld, BYTE_SIZE), \ 49 | (const void *)src + __CORE_RELO(src, fld, BYTE_OFFSET)) 50 | #else 51 | /* semantics of LSHIFT_64 assumes loading values into low-ordered bytes, so 52 | * for big-endian we need to adjust destination pointer accordingly, based on 53 | * field byte size 54 | */ 55 | #define __CORE_BITFIELD_PROBE_READ(dst, src, fld) \ 56 | bpf_probe_read_kernel( \ 57 | (void *)dst + (8 - __CORE_RELO(src, fld, BYTE_SIZE)), \ 58 | __CORE_RELO(src, fld, BYTE_SIZE), \ 59 | (const void *)src + __CORE_RELO(src, fld, BYTE_OFFSET)) 60 | #endif 61 | 62 | /* 63 | * Extract bitfield, identified by s->field, and return its value as u64. 64 | * All this is done in relocatable manner, so bitfield changes such as 65 | * signedness, bit size, offset changes, this will be handled automatically. 66 | * This version of macro is using bpf_probe_read_kernel() to read underlying 67 | * integer storage. Macro functions as an expression and its return type is 68 | * bpf_probe_read_kernel()'s return value: 0, on success, <0 on error. 69 | */ 70 | #define BPF_CORE_READ_BITFIELD_PROBED(s, field) ({ \ 71 | unsigned long long val = 0; \ 72 | \ 73 | __CORE_BITFIELD_PROBE_READ(&val, s, field); \ 74 | val <<= __CORE_RELO(s, field, LSHIFT_U64); \ 75 | if (__CORE_RELO(s, field, SIGNED)) \ 76 | val = ((long long)val) >> __CORE_RELO(s, field, RSHIFT_U64); \ 77 | else \ 78 | val = val >> __CORE_RELO(s, field, RSHIFT_U64); \ 79 | val; \ 80 | }) 81 | 82 | /* 83 | * Extract bitfield, identified by s->field, and return its value as u64. 84 | * This version of macro is using direct memory reads and should be used from 85 | * BPF program types that support such functionality (e.g., typed raw 86 | * tracepoints). 87 | */ 88 | #define BPF_CORE_READ_BITFIELD(s, field) ({ \ 89 | const void *p = (const void *)s + __CORE_RELO(s, field, BYTE_OFFSET); \ 90 | unsigned long long val; \ 91 | \ 92 | /* This is a so-called barrier_var() operation that makes specified \ 93 | * variable "a black box" for optimizing compiler. \ 94 | * It forces compiler to perform BYTE_OFFSET relocation on p and use \ 95 | * its calculated value in the switch below, instead of applying \ 96 | * the same relocation 4 times for each individual memory load. \ 97 | */ \ 98 | asm volatile("" : "=r"(p) : "0"(p)); \ 99 | \ 100 | switch (__CORE_RELO(s, field, BYTE_SIZE)) { \ 101 | case 1: val = *(const unsigned char *)p; break; \ 102 | case 2: val = *(const unsigned short *)p; break; \ 103 | case 4: val = *(const unsigned int *)p; break; \ 104 | case 8: val = *(const unsigned long long *)p; break; \ 105 | } \ 106 | val <<= __CORE_RELO(s, field, LSHIFT_U64); \ 107 | if (__CORE_RELO(s, field, SIGNED)) \ 108 | val = ((long long)val) >> __CORE_RELO(s, field, RSHIFT_U64); \ 109 | else \ 110 | val = val >> __CORE_RELO(s, field, RSHIFT_U64); \ 111 | val; \ 112 | }) 113 | 114 | #define ___bpf_field_ref1(field) (field) 115 | #define ___bpf_field_ref2(type, field) (((typeof(type) *)0)->field) 116 | #define ___bpf_field_ref(args...) \ 117 | ___bpf_apply(___bpf_field_ref, ___bpf_narg(args))(args) 118 | 119 | /* 120 | * Convenience macro to check that field actually exists in target kernel's. 121 | * Returns: 122 | * 1, if matching field is present in target kernel; 123 | * 0, if no matching field found. 124 | * 125 | * Supports two forms: 126 | * - field reference through variable access: 127 | * bpf_core_field_exists(p->my_field); 128 | * - field reference through type and field names: 129 | * bpf_core_field_exists(struct my_type, my_field). 130 | */ 131 | #define bpf_core_field_exists(field...) \ 132 | __builtin_preserve_field_info(___bpf_field_ref(field), BPF_FIELD_EXISTS) 133 | 134 | /* 135 | * Convenience macro to get the byte size of a field. Works for integers, 136 | * struct/unions, pointers, arrays, and enums. 137 | * 138 | * Supports two forms: 139 | * - field reference through variable access: 140 | * bpf_core_field_size(p->my_field); 141 | * - field reference through type and field names: 142 | * bpf_core_field_size(struct my_type, my_field). 143 | */ 144 | #define bpf_core_field_size(field...) \ 145 | __builtin_preserve_field_info(___bpf_field_ref(field), BPF_FIELD_BYTE_SIZE) 146 | 147 | /* 148 | * Convenience macro to get field's byte offset. 149 | * 150 | * Supports two forms: 151 | * - field reference through variable access: 152 | * bpf_core_field_offset(p->my_field); 153 | * - field reference through type and field names: 154 | * bpf_core_field_offset(struct my_type, my_field). 155 | */ 156 | #define bpf_core_field_offset(field...) \ 157 | __builtin_preserve_field_info(___bpf_field_ref(field), BPF_FIELD_BYTE_OFFSET) 158 | 159 | /* 160 | * Convenience macro to get BTF type ID of a specified type, using a local BTF 161 | * information. Return 32-bit unsigned integer with type ID from program's own 162 | * BTF. Always succeeds. 163 | */ 164 | #define bpf_core_type_id_local(type) \ 165 | __builtin_btf_type_id(*(typeof(type) *)0, BPF_TYPE_ID_LOCAL) 166 | 167 | /* 168 | * Convenience macro to get BTF type ID of a target kernel's type that matches 169 | * specified local type. 170 | * Returns: 171 | * - valid 32-bit unsigned type ID in kernel BTF; 172 | * - 0, if no matching type was found in a target kernel BTF. 173 | */ 174 | #define bpf_core_type_id_kernel(type) \ 175 | __builtin_btf_type_id(*(typeof(type) *)0, BPF_TYPE_ID_TARGET) 176 | 177 | /* 178 | * Convenience macro to check that provided named type 179 | * (struct/union/enum/typedef) exists in a target kernel. 180 | * Returns: 181 | * 1, if such type is present in target kernel's BTF; 182 | * 0, if no matching type is found. 183 | */ 184 | #define bpf_core_type_exists(type) \ 185 | __builtin_preserve_type_info(*(typeof(type) *)0, BPF_TYPE_EXISTS) 186 | 187 | /* 188 | * Convenience macro to check that provided named type 189 | * (struct/union/enum/typedef) "matches" that in a target kernel. 190 | * Returns: 191 | * 1, if the type matches in the target kernel's BTF; 192 | * 0, if the type does not match any in the target kernel 193 | */ 194 | #define bpf_core_type_matches(type) \ 195 | __builtin_preserve_type_info(*(typeof(type) *)0, BPF_TYPE_MATCHES) 196 | 197 | /* 198 | * Convenience macro to get the byte size of a provided named type 199 | * (struct/union/enum/typedef) in a target kernel. 200 | * Returns: 201 | * >= 0 size (in bytes), if type is present in target kernel's BTF; 202 | * 0, if no matching type is found. 203 | */ 204 | #define bpf_core_type_size(type) \ 205 | __builtin_preserve_type_info(*(typeof(type) *)0, BPF_TYPE_SIZE) 206 | 207 | /* 208 | * Convenience macro to check that provided enumerator value is defined in 209 | * a target kernel. 210 | * Returns: 211 | * 1, if specified enum type and its enumerator value are present in target 212 | * kernel's BTF; 213 | * 0, if no matching enum and/or enum value within that enum is found. 214 | */ 215 | #define bpf_core_enum_value_exists(enum_type, enum_value) \ 216 | __builtin_preserve_enum_value(*(typeof(enum_type) *)enum_value, BPF_ENUMVAL_EXISTS) 217 | 218 | /* 219 | * Convenience macro to get the integer value of an enumerator value in 220 | * a target kernel. 221 | * Returns: 222 | * 64-bit value, if specified enum type and its enumerator value are 223 | * present in target kernel's BTF; 224 | * 0, if no matching enum and/or enum value within that enum is found. 225 | */ 226 | #define bpf_core_enum_value(enum_type, enum_value) \ 227 | __builtin_preserve_enum_value(*(typeof(enum_type) *)enum_value, BPF_ENUMVAL_VALUE) 228 | 229 | /* 230 | * bpf_core_read() abstracts away bpf_probe_read_kernel() call and captures 231 | * offset relocation for source address using __builtin_preserve_access_index() 232 | * built-in, provided by Clang. 233 | * 234 | * __builtin_preserve_access_index() takes as an argument an expression of 235 | * taking an address of a field within struct/union. It makes compiler emit 236 | * a relocation, which records BTF type ID describing root struct/union and an 237 | * accessor string which describes exact embedded field that was used to take 238 | * an address. See detailed description of this relocation format and 239 | * semantics in comments to struct bpf_field_reloc in libbpf_internal.h. 240 | * 241 | * This relocation allows libbpf to adjust BPF instruction to use correct 242 | * actual field offset, based on target kernel BTF type that matches original 243 | * (local) BTF, used to record relocation. 244 | */ 245 | #define bpf_core_read(dst, sz, src) \ 246 | bpf_probe_read_kernel(dst, sz, (const void *)__builtin_preserve_access_index(src)) 247 | 248 | /* NOTE: see comments for BPF_CORE_READ_USER() about the proper types use. */ 249 | #define bpf_core_read_user(dst, sz, src) \ 250 | bpf_probe_read_user(dst, sz, (const void *)__builtin_preserve_access_index(src)) 251 | /* 252 | * bpf_core_read_str() is a thin wrapper around bpf_probe_read_str() 253 | * additionally emitting BPF CO-RE field relocation for specified source 254 | * argument. 255 | */ 256 | #define bpf_core_read_str(dst, sz, src) \ 257 | bpf_probe_read_kernel_str(dst, sz, (const void *)__builtin_preserve_access_index(src)) 258 | 259 | /* NOTE: see comments for BPF_CORE_READ_USER() about the proper types use. */ 260 | #define bpf_core_read_user_str(dst, sz, src) \ 261 | bpf_probe_read_user_str(dst, sz, (const void *)__builtin_preserve_access_index(src)) 262 | 263 | #define ___concat(a, b) a ## b 264 | #define ___apply(fn, n) ___concat(fn, n) 265 | #define ___nth(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, __11, N, ...) N 266 | 267 | /* 268 | * return number of provided arguments; used for switch-based variadic macro 269 | * definitions (see ___last, ___arrow, etc below) 270 | */ 271 | #define ___narg(...) ___nth(_, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 272 | /* 273 | * return 0 if no arguments are passed, N - otherwise; used for 274 | * recursively-defined macros to specify termination (0) case, and generic 275 | * (N) case (e.g., ___read_ptrs, ___core_read) 276 | */ 277 | #define ___empty(...) ___nth(_, ##__VA_ARGS__, N, N, N, N, N, N, N, N, N, N, 0) 278 | 279 | #define ___last1(x) x 280 | #define ___last2(a, x) x 281 | #define ___last3(a, b, x) x 282 | #define ___last4(a, b, c, x) x 283 | #define ___last5(a, b, c, d, x) x 284 | #define ___last6(a, b, c, d, e, x) x 285 | #define ___last7(a, b, c, d, e, f, x) x 286 | #define ___last8(a, b, c, d, e, f, g, x) x 287 | #define ___last9(a, b, c, d, e, f, g, h, x) x 288 | #define ___last10(a, b, c, d, e, f, g, h, i, x) x 289 | #define ___last(...) ___apply(___last, ___narg(__VA_ARGS__))(__VA_ARGS__) 290 | 291 | #define ___nolast2(a, _) a 292 | #define ___nolast3(a, b, _) a, b 293 | #define ___nolast4(a, b, c, _) a, b, c 294 | #define ___nolast5(a, b, c, d, _) a, b, c, d 295 | #define ___nolast6(a, b, c, d, e, _) a, b, c, d, e 296 | #define ___nolast7(a, b, c, d, e, f, _) a, b, c, d, e, f 297 | #define ___nolast8(a, b, c, d, e, f, g, _) a, b, c, d, e, f, g 298 | #define ___nolast9(a, b, c, d, e, f, g, h, _) a, b, c, d, e, f, g, h 299 | #define ___nolast10(a, b, c, d, e, f, g, h, i, _) a, b, c, d, e, f, g, h, i 300 | #define ___nolast(...) ___apply(___nolast, ___narg(__VA_ARGS__))(__VA_ARGS__) 301 | 302 | #define ___arrow1(a) a 303 | #define ___arrow2(a, b) a->b 304 | #define ___arrow3(a, b, c) a->b->c 305 | #define ___arrow4(a, b, c, d) a->b->c->d 306 | #define ___arrow5(a, b, c, d, e) a->b->c->d->e 307 | #define ___arrow6(a, b, c, d, e, f) a->b->c->d->e->f 308 | #define ___arrow7(a, b, c, d, e, f, g) a->b->c->d->e->f->g 309 | #define ___arrow8(a, b, c, d, e, f, g, h) a->b->c->d->e->f->g->h 310 | #define ___arrow9(a, b, c, d, e, f, g, h, i) a->b->c->d->e->f->g->h->i 311 | #define ___arrow10(a, b, c, d, e, f, g, h, i, j) a->b->c->d->e->f->g->h->i->j 312 | #define ___arrow(...) ___apply(___arrow, ___narg(__VA_ARGS__))(__VA_ARGS__) 313 | 314 | #define ___type(...) typeof(___arrow(__VA_ARGS__)) 315 | 316 | #define ___read(read_fn, dst, src_type, src, accessor) \ 317 | read_fn((void *)(dst), sizeof(*(dst)), &((src_type)(src))->accessor) 318 | 319 | /* "recursively" read a sequence of inner pointers using local __t var */ 320 | #define ___rd_first(fn, src, a) ___read(fn, &__t, ___type(src), src, a); 321 | #define ___rd_last(fn, ...) \ 322 | ___read(fn, &__t, ___type(___nolast(__VA_ARGS__)), __t, ___last(__VA_ARGS__)); 323 | #define ___rd_p1(fn, ...) const void *__t; ___rd_first(fn, __VA_ARGS__) 324 | #define ___rd_p2(fn, ...) ___rd_p1(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 325 | #define ___rd_p3(fn, ...) ___rd_p2(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 326 | #define ___rd_p4(fn, ...) ___rd_p3(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 327 | #define ___rd_p5(fn, ...) ___rd_p4(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 328 | #define ___rd_p6(fn, ...) ___rd_p5(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 329 | #define ___rd_p7(fn, ...) ___rd_p6(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 330 | #define ___rd_p8(fn, ...) ___rd_p7(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 331 | #define ___rd_p9(fn, ...) ___rd_p8(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__) 332 | #define ___read_ptrs(fn, src, ...) \ 333 | ___apply(___rd_p, ___narg(__VA_ARGS__))(fn, src, __VA_ARGS__) 334 | 335 | #define ___core_read0(fn, fn_ptr, dst, src, a) \ 336 | ___read(fn, dst, ___type(src), src, a); 337 | #define ___core_readN(fn, fn_ptr, dst, src, ...) \ 338 | ___read_ptrs(fn_ptr, src, ___nolast(__VA_ARGS__)) \ 339 | ___read(fn, dst, ___type(src, ___nolast(__VA_ARGS__)), __t, \ 340 | ___last(__VA_ARGS__)); 341 | #define ___core_read(fn, fn_ptr, dst, src, a, ...) \ 342 | ___apply(___core_read, ___empty(__VA_ARGS__))(fn, fn_ptr, dst, \ 343 | src, a, ##__VA_ARGS__) 344 | 345 | /* 346 | * BPF_CORE_READ_INTO() is a more performance-conscious variant of 347 | * BPF_CORE_READ(), in which final field is read into user-provided storage. 348 | * See BPF_CORE_READ() below for more details on general usage. 349 | */ 350 | #define BPF_CORE_READ_INTO(dst, src, a, ...) ({ \ 351 | ___core_read(bpf_core_read, bpf_core_read, \ 352 | dst, (src), a, ##__VA_ARGS__) \ 353 | }) 354 | 355 | /* 356 | * Variant of BPF_CORE_READ_INTO() for reading from user-space memory. 357 | * 358 | * NOTE: see comments for BPF_CORE_READ_USER() about the proper types use. 359 | */ 360 | #define BPF_CORE_READ_USER_INTO(dst, src, a, ...) ({ \ 361 | ___core_read(bpf_core_read_user, bpf_core_read_user, \ 362 | dst, (src), a, ##__VA_ARGS__) \ 363 | }) 364 | 365 | /* Non-CO-RE variant of BPF_CORE_READ_INTO() */ 366 | #define BPF_PROBE_READ_INTO(dst, src, a, ...) ({ \ 367 | ___core_read(bpf_probe_read_kernel, bpf_probe_read_kernel, \ 368 | dst, (src), a, ##__VA_ARGS__) \ 369 | }) 370 | 371 | /* Non-CO-RE variant of BPF_CORE_READ_USER_INTO(). 372 | * 373 | * As no CO-RE relocations are emitted, source types can be arbitrary and are 374 | * not restricted to kernel types only. 375 | */ 376 | #define BPF_PROBE_READ_USER_INTO(dst, src, a, ...) ({ \ 377 | ___core_read(bpf_probe_read_user, bpf_probe_read_user, \ 378 | dst, (src), a, ##__VA_ARGS__) \ 379 | }) 380 | 381 | /* 382 | * BPF_CORE_READ_STR_INTO() does same "pointer chasing" as 383 | * BPF_CORE_READ() for intermediate pointers, but then executes (and returns 384 | * corresponding error code) bpf_core_read_str() for final string read. 385 | */ 386 | #define BPF_CORE_READ_STR_INTO(dst, src, a, ...) ({ \ 387 | ___core_read(bpf_core_read_str, bpf_core_read, \ 388 | dst, (src), a, ##__VA_ARGS__) \ 389 | }) 390 | 391 | /* 392 | * Variant of BPF_CORE_READ_STR_INTO() for reading from user-space memory. 393 | * 394 | * NOTE: see comments for BPF_CORE_READ_USER() about the proper types use. 395 | */ 396 | #define BPF_CORE_READ_USER_STR_INTO(dst, src, a, ...) ({ \ 397 | ___core_read(bpf_core_read_user_str, bpf_core_read_user, \ 398 | dst, (src), a, ##__VA_ARGS__) \ 399 | }) 400 | 401 | /* Non-CO-RE variant of BPF_CORE_READ_STR_INTO() */ 402 | #define BPF_PROBE_READ_STR_INTO(dst, src, a, ...) ({ \ 403 | ___core_read(bpf_probe_read_kernel_str, bpf_probe_read_kernel, \ 404 | dst, (src), a, ##__VA_ARGS__) \ 405 | }) 406 | 407 | /* 408 | * Non-CO-RE variant of BPF_CORE_READ_USER_STR_INTO(). 409 | * 410 | * As no CO-RE relocations are emitted, source types can be arbitrary and are 411 | * not restricted to kernel types only. 412 | */ 413 | #define BPF_PROBE_READ_USER_STR_INTO(dst, src, a, ...) ({ \ 414 | ___core_read(bpf_probe_read_user_str, bpf_probe_read_user, \ 415 | dst, (src), a, ##__VA_ARGS__) \ 416 | }) 417 | 418 | /* 419 | * BPF_CORE_READ() is used to simplify BPF CO-RE relocatable read, especially 420 | * when there are few pointer chasing steps. 421 | * E.g., what in non-BPF world (or in BPF w/ BCC) would be something like: 422 | * int x = s->a.b.c->d.e->f->g; 423 | * can be succinctly achieved using BPF_CORE_READ as: 424 | * int x = BPF_CORE_READ(s, a.b.c, d.e, f, g); 425 | * 426 | * BPF_CORE_READ will decompose above statement into 4 bpf_core_read (BPF 427 | * CO-RE relocatable bpf_probe_read_kernel() wrapper) calls, logically 428 | * equivalent to: 429 | * 1. const void *__t = s->a.b.c; 430 | * 2. __t = __t->d.e; 431 | * 3. __t = __t->f; 432 | * 4. return __t->g; 433 | * 434 | * Equivalence is logical, because there is a heavy type casting/preservation 435 | * involved, as well as all the reads are happening through 436 | * bpf_probe_read_kernel() calls using __builtin_preserve_access_index() to 437 | * emit CO-RE relocations. 438 | * 439 | * N.B. Only up to 9 "field accessors" are supported, which should be more 440 | * than enough for any practical purpose. 441 | */ 442 | #define BPF_CORE_READ(src, a, ...) ({ \ 443 | ___type((src), a, ##__VA_ARGS__) __r; \ 444 | BPF_CORE_READ_INTO(&__r, (src), a, ##__VA_ARGS__); \ 445 | __r; \ 446 | }) 447 | 448 | /* 449 | * Variant of BPF_CORE_READ() for reading from user-space memory. 450 | * 451 | * NOTE: all the source types involved are still *kernel types* and need to 452 | * exist in kernel (or kernel module) BTF, otherwise CO-RE relocation will 453 | * fail. Custom user types are not relocatable with CO-RE. 454 | * The typical situation in which BPF_CORE_READ_USER() might be used is to 455 | * read kernel UAPI types from the user-space memory passed in as a syscall 456 | * input argument. 457 | */ 458 | #define BPF_CORE_READ_USER(src, a, ...) ({ \ 459 | ___type((src), a, ##__VA_ARGS__) __r; \ 460 | BPF_CORE_READ_USER_INTO(&__r, (src), a, ##__VA_ARGS__); \ 461 | __r; \ 462 | }) 463 | 464 | /* Non-CO-RE variant of BPF_CORE_READ() */ 465 | #define BPF_PROBE_READ(src, a, ...) ({ \ 466 | ___type((src), a, ##__VA_ARGS__) __r; \ 467 | BPF_PROBE_READ_INTO(&__r, (src), a, ##__VA_ARGS__); \ 468 | __r; \ 469 | }) 470 | 471 | /* 472 | * Non-CO-RE variant of BPF_CORE_READ_USER(). 473 | * 474 | * As no CO-RE relocations are emitted, source types can be arbitrary and are 475 | * not restricted to kernel types only. 476 | */ 477 | #define BPF_PROBE_READ_USER(src, a, ...) ({ \ 478 | ___type((src), a, ##__VA_ARGS__) __r; \ 479 | BPF_PROBE_READ_USER_INTO(&__r, (src), a, ##__VA_ARGS__); \ 480 | __r; \ 481 | }) 482 | 483 | #endif 484 | 485 | -------------------------------------------------------------------------------- /include/bpf_endian.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | #ifndef __BPF_ENDIAN__ 3 | #define __BPF_ENDIAN__ 4 | 5 | /* 6 | * Isolate byte #n and put it into byte #m, for __u##b type. 7 | * E.g., moving byte #6 (nnnnnnnn) into byte #1 (mmmmmmmm) for __u64: 8 | * 1) xxxxxxxx nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 9 | * 2) nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 00000000 10 | * 3) 00000000 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 11 | * 4) 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 00000000 12 | */ 13 | #define ___bpf_mvb(x, b, n, m) ((__u##b)(x) << (b-(n+1)*8) >> (b-8) << (m*8)) 14 | 15 | #define ___bpf_swab16(x) ((__u16)( \ 16 | ___bpf_mvb(x, 16, 0, 1) | \ 17 | ___bpf_mvb(x, 16, 1, 0))) 18 | 19 | #define ___bpf_swab32(x) ((__u32)( \ 20 | ___bpf_mvb(x, 32, 0, 3) | \ 21 | ___bpf_mvb(x, 32, 1, 2) | \ 22 | ___bpf_mvb(x, 32, 2, 1) | \ 23 | ___bpf_mvb(x, 32, 3, 0))) 24 | 25 | #define ___bpf_swab64(x) ((__u64)( \ 26 | ___bpf_mvb(x, 64, 0, 7) | \ 27 | ___bpf_mvb(x, 64, 1, 6) | \ 28 | ___bpf_mvb(x, 64, 2, 5) | \ 29 | ___bpf_mvb(x, 64, 3, 4) | \ 30 | ___bpf_mvb(x, 64, 4, 3) | \ 31 | ___bpf_mvb(x, 64, 5, 2) | \ 32 | ___bpf_mvb(x, 64, 6, 1) | \ 33 | ___bpf_mvb(x, 64, 7, 0))) 34 | 35 | /* LLVM's BPF target selects the endianness of the CPU 36 | * it compiles on, or the user specifies (bpfel/bpfeb), 37 | * respectively. The used __BYTE_ORDER__ is defined by 38 | * the compiler, we cannot rely on __BYTE_ORDER from 39 | * libc headers, since it doesn't reflect the actual 40 | * requested byte order. 41 | * 42 | * Note, LLVM's BPF target has different __builtin_bswapX() 43 | * semantics. It does map to BPF_ALU | BPF_END | BPF_TO_BE 44 | * in bpfel and bpfeb case, which means below, that we map 45 | * to cpu_to_be16(). We could use it unconditionally in BPF 46 | * case, but better not rely on it, so that this header here 47 | * can be used from application and BPF program side, which 48 | * use different targets. 49 | */ 50 | #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 51 | # define __bpf_ntohs(x) __builtin_bswap16(x) 52 | # define __bpf_htons(x) __builtin_bswap16(x) 53 | # define __bpf_constant_ntohs(x) ___bpf_swab16(x) 54 | # define __bpf_constant_htons(x) ___bpf_swab16(x) 55 | # define __bpf_ntohl(x) __builtin_bswap32(x) 56 | # define __bpf_htonl(x) __builtin_bswap32(x) 57 | # define __bpf_constant_ntohl(x) ___bpf_swab32(x) 58 | # define __bpf_constant_htonl(x) ___bpf_swab32(x) 59 | # define __bpf_be64_to_cpu(x) __builtin_bswap64(x) 60 | # define __bpf_cpu_to_be64(x) __builtin_bswap64(x) 61 | # define __bpf_constant_be64_to_cpu(x) ___bpf_swab64(x) 62 | # define __bpf_constant_cpu_to_be64(x) ___bpf_swab64(x) 63 | #elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ 64 | # define __bpf_ntohs(x) (x) 65 | # define __bpf_htons(x) (x) 66 | # define __bpf_constant_ntohs(x) (x) 67 | # define __bpf_constant_htons(x) (x) 68 | # define __bpf_ntohl(x) (x) 69 | # define __bpf_htonl(x) (x) 70 | # define __bpf_constant_ntohl(x) (x) 71 | # define __bpf_constant_htonl(x) (x) 72 | # define __bpf_be64_to_cpu(x) (x) 73 | # define __bpf_cpu_to_be64(x) (x) 74 | # define __bpf_constant_be64_to_cpu(x) (x) 75 | # define __bpf_constant_cpu_to_be64(x) (x) 76 | #else 77 | # error "Fix your compiler's __BYTE_ORDER__?!" 78 | #endif 79 | 80 | #define bpf_htons(x) \ 81 | (__builtin_constant_p(x) ? \ 82 | __bpf_constant_htons(x) : __bpf_htons(x)) 83 | #define bpf_ntohs(x) \ 84 | (__builtin_constant_p(x) ? \ 85 | __bpf_constant_ntohs(x) : __bpf_ntohs(x)) 86 | #define bpf_htonl(x) \ 87 | (__builtin_constant_p(x) ? \ 88 | __bpf_constant_htonl(x) : __bpf_htonl(x)) 89 | #define bpf_ntohl(x) \ 90 | (__builtin_constant_p(x) ? \ 91 | __bpf_constant_ntohl(x) : __bpf_ntohl(x)) 92 | #define bpf_cpu_to_be64(x) \ 93 | (__builtin_constant_p(x) ? \ 94 | __bpf_constant_cpu_to_be64(x) : __bpf_cpu_to_be64(x)) 95 | #define bpf_be64_to_cpu(x) \ 96 | (__builtin_constant_p(x) ? \ 97 | __bpf_constant_be64_to_cpu(x) : __bpf_be64_to_cpu(x)) 98 | 99 | #endif /* __BPF_ENDIAN__ */ 100 | -------------------------------------------------------------------------------- /include/bpf_helpers.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | #ifndef __BPF_HELPERS__ 3 | #define __BPF_HELPERS__ 4 | 5 | /* 6 | * Note that bpf programs need to include either 7 | * vmlinux.h (auto-generated from BTF) or linux/types.h 8 | * in advance since bpf_helper_defs.h uses such types 9 | * as __u64. 10 | */ 11 | #include "bpf_helper_defs.h" 12 | 13 | #define __uint(name, val) int (*name)[val] 14 | #define __type(name, val) typeof(val) *name 15 | #define __array(name, val) typeof(val) *name[] 16 | 17 | /* 18 | * Helper macro to place programs, maps, license in 19 | * different sections in elf_bpf file. Section names 20 | * are interpreted by libbpf depending on the context (BPF programs, BPF maps, 21 | * extern variables, etc). 22 | * To allow use of SEC() with externs (e.g., for extern .maps declarations), 23 | * make sure __attribute__((unused)) doesn't trigger compilation warning. 24 | */ 25 | #define SEC(name) \ 26 | _Pragma("GCC diagnostic push") \ 27 | _Pragma("GCC diagnostic ignored \"-Wignored-attributes\"") \ 28 | __attribute__((section(name), used)) \ 29 | _Pragma("GCC diagnostic pop") \ 30 | 31 | /* Avoid 'linux/stddef.h' definition of '__always_inline'. */ 32 | #undef __always_inline 33 | #define __always_inline inline __attribute__((always_inline)) 34 | 35 | #ifndef __noinline 36 | #define __noinline __attribute__((noinline)) 37 | #endif 38 | #ifndef __weak 39 | #define __weak __attribute__((weak)) 40 | #endif 41 | 42 | /* 43 | * Use __hidden attribute to mark a non-static BPF subprogram effectively 44 | * static for BPF verifier's verification algorithm purposes, allowing more 45 | * extensive and permissive BPF verification process, taking into account 46 | * subprogram's caller context. 47 | */ 48 | #define __hidden __attribute__((visibility("hidden"))) 49 | 50 | /* When utilizing vmlinux.h with BPF CO-RE, user BPF programs can't include 51 | * any system-level headers (such as stddef.h, linux/version.h, etc), and 52 | * commonly-used macros like NULL and KERNEL_VERSION aren't available through 53 | * vmlinux.h. This just adds unnecessary hurdles and forces users to re-define 54 | * them on their own. So as a convenience, provide such definitions here. 55 | */ 56 | #ifndef NULL 57 | #define NULL ((void *)0) 58 | #endif 59 | 60 | #ifndef KERNEL_VERSION 61 | #define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c))) 62 | #endif 63 | 64 | /* 65 | * Helper macros to manipulate data structures 66 | */ 67 | #ifndef offsetof 68 | #define offsetof(TYPE, MEMBER) ((unsigned long)&((TYPE *)0)->MEMBER) 69 | #endif 70 | #ifndef container_of 71 | #define container_of(ptr, type, member) \ 72 | ({ \ 73 | void *__mptr = (void *)(ptr); \ 74 | ((type *)(__mptr - offsetof(type, member))); \ 75 | }) 76 | #endif 77 | 78 | /* 79 | * Helper macro to throw a compilation error if __bpf_unreachable() gets 80 | * built into the resulting code. This works given BPF back end does not 81 | * implement __builtin_trap(). This is useful to assert that certain paths 82 | * of the program code are never used and hence eliminated by the compiler. 83 | * 84 | * For example, consider a switch statement that covers known cases used by 85 | * the program. __bpf_unreachable() can then reside in the default case. If 86 | * the program gets extended such that a case is not covered in the switch 87 | * statement, then it will throw a build error due to the default case not 88 | * being compiled out. 89 | */ 90 | #ifndef __bpf_unreachable 91 | # define __bpf_unreachable() __builtin_trap() 92 | #endif 93 | 94 | /* 95 | * Helper function to perform a tail call with a constant/immediate map slot. 96 | */ 97 | #if __clang_major__ >= 8 && defined(__bpf__) 98 | static __always_inline void 99 | bpf_tail_call_static(void *ctx, const void *map, const __u32 slot) 100 | { 101 | if (!__builtin_constant_p(slot)) 102 | __bpf_unreachable(); 103 | 104 | /* 105 | * Provide a hard guarantee that LLVM won't optimize setting r2 (map 106 | * pointer) and r3 (constant map index) from _different paths_ ending 107 | * up at the _same_ call insn as otherwise we won't be able to use the 108 | * jmpq/nopl retpoline-free patching by the x86-64 JIT in the kernel 109 | * given they mismatch. See also d2e4c1e6c294 ("bpf: Constant map key 110 | * tracking for prog array pokes") for details on verifier tracking. 111 | * 112 | * Note on clobber list: we need to stay in-line with BPF calling 113 | * convention, so even if we don't end up using r0, r4, r5, we need 114 | * to mark them as clobber so that LLVM doesn't end up using them 115 | * before / after the call. 116 | */ 117 | asm volatile("r1 = %[ctx]\n\t" 118 | "r2 = %[map]\n\t" 119 | "r3 = %[slot]\n\t" 120 | "call 12" 121 | :: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot) 122 | : "r0", "r1", "r2", "r3", "r4", "r5"); 123 | } 124 | #endif 125 | 126 | /* 127 | * Helper structure used by eBPF C program 128 | * to describe BPF map attributes to libbpf loader 129 | */ 130 | struct bpf_map_def { 131 | unsigned int type; 132 | unsigned int key_size; 133 | unsigned int value_size; 134 | unsigned int max_entries; 135 | unsigned int map_flags; 136 | }; 137 | 138 | enum libbpf_pin_type { 139 | LIBBPF_PIN_NONE, 140 | /* PIN_BY_NAME: pin maps by name (in /sys/fs/bpf by default) */ 141 | LIBBPF_PIN_BY_NAME, 142 | }; 143 | 144 | enum libbpf_tristate { 145 | TRI_NO = 0, 146 | TRI_YES = 1, 147 | TRI_MODULE = 2, 148 | }; 149 | 150 | #define __kconfig __attribute__((section(".kconfig"))) 151 | #define __ksym __attribute__((section(".ksyms"))) 152 | 153 | #ifndef ___bpf_concat 154 | #define ___bpf_concat(a, b) a ## b 155 | #endif 156 | #ifndef ___bpf_apply 157 | #define ___bpf_apply(fn, n) ___bpf_concat(fn, n) 158 | #endif 159 | #ifndef ___bpf_nth 160 | #define ___bpf_nth(_, _1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, N, ...) N 161 | #endif 162 | #ifndef ___bpf_narg 163 | #define ___bpf_narg(...) \ 164 | ___bpf_nth(_, ##__VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 165 | #endif 166 | 167 | #define ___bpf_fill0(arr, p, x) do {} while (0) 168 | #define ___bpf_fill1(arr, p, x) arr[p] = x 169 | #define ___bpf_fill2(arr, p, x, args...) arr[p] = x; ___bpf_fill1(arr, p + 1, args) 170 | #define ___bpf_fill3(arr, p, x, args...) arr[p] = x; ___bpf_fill2(arr, p + 1, args) 171 | #define ___bpf_fill4(arr, p, x, args...) arr[p] = x; ___bpf_fill3(arr, p + 1, args) 172 | #define ___bpf_fill5(arr, p, x, args...) arr[p] = x; ___bpf_fill4(arr, p + 1, args) 173 | #define ___bpf_fill6(arr, p, x, args...) arr[p] = x; ___bpf_fill5(arr, p + 1, args) 174 | #define ___bpf_fill7(arr, p, x, args...) arr[p] = x; ___bpf_fill6(arr, p + 1, args) 175 | #define ___bpf_fill8(arr, p, x, args...) arr[p] = x; ___bpf_fill7(arr, p + 1, args) 176 | #define ___bpf_fill9(arr, p, x, args...) arr[p] = x; ___bpf_fill8(arr, p + 1, args) 177 | #define ___bpf_fill10(arr, p, x, args...) arr[p] = x; ___bpf_fill9(arr, p + 1, args) 178 | #define ___bpf_fill11(arr, p, x, args...) arr[p] = x; ___bpf_fill10(arr, p + 1, args) 179 | #define ___bpf_fill12(arr, p, x, args...) arr[p] = x; ___bpf_fill11(arr, p + 1, args) 180 | #define ___bpf_fill(arr, args...) \ 181 | ___bpf_apply(___bpf_fill, ___bpf_narg(args))(arr, 0, args) 182 | 183 | /* 184 | * BPF_SEQ_PRINTF to wrap bpf_seq_printf to-be-printed values 185 | * in a structure. 186 | */ 187 | #define BPF_SEQ_PRINTF(seq, fmt, args...) \ 188 | ({ \ 189 | static const char ___fmt[] = fmt; \ 190 | unsigned long long ___param[___bpf_narg(args)]; \ 191 | \ 192 | _Pragma("GCC diagnostic push") \ 193 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 194 | ___bpf_fill(___param, args); \ 195 | _Pragma("GCC diagnostic pop") \ 196 | \ 197 | bpf_seq_printf(seq, ___fmt, sizeof(___fmt), \ 198 | ___param, sizeof(___param)); \ 199 | }) 200 | 201 | /* 202 | * BPF_SNPRINTF wraps the bpf_snprintf helper with variadic arguments instead of 203 | * an array of u64. 204 | */ 205 | #define BPF_SNPRINTF(out, out_size, fmt, args...) \ 206 | ({ \ 207 | static const char ___fmt[] = fmt; \ 208 | unsigned long long ___param[___bpf_narg(args)]; \ 209 | \ 210 | _Pragma("GCC diagnostic push") \ 211 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 212 | ___bpf_fill(___param, args); \ 213 | _Pragma("GCC diagnostic pop") \ 214 | \ 215 | bpf_snprintf(out, out_size, ___fmt, \ 216 | ___param, sizeof(___param)); \ 217 | }) 218 | 219 | #ifdef BPF_NO_GLOBAL_DATA 220 | #define BPF_PRINTK_FMT_MOD 221 | #else 222 | #define BPF_PRINTK_FMT_MOD static const 223 | #endif 224 | 225 | #define __bpf_printk(fmt, ...) \ 226 | ({ \ 227 | BPF_PRINTK_FMT_MOD char ____fmt[] = fmt; \ 228 | bpf_trace_printk(____fmt, sizeof(____fmt), \ 229 | ##__VA_ARGS__); \ 230 | }) 231 | 232 | /* 233 | * __bpf_vprintk wraps the bpf_trace_vprintk helper with variadic arguments 234 | * instead of an array of u64. 235 | */ 236 | #define __bpf_vprintk(fmt, args...) \ 237 | ({ \ 238 | static const char ___fmt[] = fmt; \ 239 | unsigned long long ___param[___bpf_narg(args)]; \ 240 | \ 241 | _Pragma("GCC diagnostic push") \ 242 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 243 | ___bpf_fill(___param, args); \ 244 | _Pragma("GCC diagnostic pop") \ 245 | \ 246 | bpf_trace_vprintk(___fmt, sizeof(___fmt), \ 247 | ___param, sizeof(___param)); \ 248 | }) 249 | 250 | /* Use __bpf_printk when bpf_printk call has 3 or fewer fmt args 251 | * Otherwise use __bpf_vprintk 252 | */ 253 | #define ___bpf_pick_printk(...) \ 254 | ___bpf_nth(_, ##__VA_ARGS__, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \ 255 | __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \ 256 | __bpf_vprintk, __bpf_vprintk, __bpf_printk /*3*/, __bpf_printk /*2*/,\ 257 | __bpf_printk /*1*/, __bpf_printk /*0*/) 258 | 259 | /* Helper macro to print out debug messages */ 260 | #define bpf_printk(fmt, args...) ___bpf_pick_printk(args)(fmt, ##args) 261 | 262 | #endif 263 | -------------------------------------------------------------------------------- /include/bpf_tracing.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | #ifndef __BPF_TRACING_H__ 3 | #define __BPF_TRACING_H__ 4 | 5 | /* Scan the ARCH passed in from ARCH env variable (see Makefile) */ 6 | #if defined(__TARGET_ARCH_x86) 7 | #define bpf_target_x86 8 | #define bpf_target_defined 9 | #elif defined(__TARGET_ARCH_s390) 10 | #define bpf_target_s390 11 | #define bpf_target_defined 12 | #elif defined(__TARGET_ARCH_arm) 13 | #define bpf_target_arm 14 | #define bpf_target_defined 15 | #elif defined(__TARGET_ARCH_arm64) 16 | #define bpf_target_arm64 17 | #define bpf_target_defined 18 | #elif defined(__TARGET_ARCH_mips) 19 | #define bpf_target_mips 20 | #define bpf_target_defined 21 | #elif defined(__TARGET_ARCH_powerpc) 22 | #define bpf_target_powerpc 23 | #define bpf_target_defined 24 | #elif defined(__TARGET_ARCH_sparc) 25 | #define bpf_target_sparc 26 | #define bpf_target_defined 27 | #elif defined(__TARGET_ARCH_riscv) 28 | #define bpf_target_riscv 29 | #define bpf_target_defined 30 | #else 31 | 32 | /* Fall back to what the compiler says */ 33 | #if defined(__x86_64__) 34 | #define bpf_target_x86 35 | #define bpf_target_defined 36 | #elif defined(__s390__) 37 | #define bpf_target_s390 38 | #define bpf_target_defined 39 | #elif defined(__arm__) 40 | #define bpf_target_arm 41 | #define bpf_target_defined 42 | #elif defined(__aarch64__) 43 | #define bpf_target_arm64 44 | #define bpf_target_defined 45 | #elif defined(__mips__) 46 | #define bpf_target_mips 47 | #define bpf_target_defined 48 | #elif defined(__powerpc__) 49 | #define bpf_target_powerpc 50 | #define bpf_target_defined 51 | #elif defined(__sparc__) 52 | #define bpf_target_sparc 53 | #define bpf_target_defined 54 | #elif defined(__riscv) && __riscv_xlen == 64 55 | #define bpf_target_riscv 56 | #define bpf_target_defined 57 | #endif /* no compiler target */ 58 | 59 | #endif 60 | 61 | #ifndef __BPF_TARGET_MISSING 62 | #define __BPF_TARGET_MISSING "GCC error \"Must specify a BPF target arch via __TARGET_ARCH_xxx\"" 63 | #endif 64 | 65 | #if defined(bpf_target_x86) 66 | 67 | #if defined(__KERNEL__) || defined(__VMLINUX_H__) 68 | 69 | #define PT_REGS_PARM1(x) ((x)->di) 70 | #define PT_REGS_PARM2(x) ((x)->si) 71 | #define PT_REGS_PARM3(x) ((x)->dx) 72 | #define PT_REGS_PARM4(x) ((x)->cx) 73 | #define PT_REGS_PARM5(x) ((x)->r8) 74 | #define PT_REGS_RET(x) ((x)->sp) 75 | #define PT_REGS_FP(x) ((x)->bp) 76 | #define PT_REGS_RC(x) ((x)->ax) 77 | #define PT_REGS_SP(x) ((x)->sp) 78 | #define PT_REGS_IP(x) ((x)->ip) 79 | 80 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((x), di) 81 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((x), si) 82 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((x), dx) 83 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((x), cx) 84 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((x), r8) 85 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((x), sp) 86 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((x), bp) 87 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((x), ax) 88 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((x), sp) 89 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), ip) 90 | 91 | #else 92 | 93 | #ifdef __i386__ 94 | /* i386 kernel is built with -mregparm=3 */ 95 | #define PT_REGS_PARM1(x) ((x)->eax) 96 | #define PT_REGS_PARM2(x) ((x)->edx) 97 | #define PT_REGS_PARM3(x) ((x)->ecx) 98 | #define PT_REGS_PARM4(x) 0 99 | #define PT_REGS_PARM5(x) 0 100 | #define PT_REGS_RET(x) ((x)->esp) 101 | #define PT_REGS_FP(x) ((x)->ebp) 102 | #define PT_REGS_RC(x) ((x)->eax) 103 | #define PT_REGS_SP(x) ((x)->esp) 104 | #define PT_REGS_IP(x) ((x)->eip) 105 | 106 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((x), eax) 107 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((x), edx) 108 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((x), ecx) 109 | #define PT_REGS_PARM4_CORE(x) 0 110 | #define PT_REGS_PARM5_CORE(x) 0 111 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((x), esp) 112 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((x), ebp) 113 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((x), eax) 114 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((x), esp) 115 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), eip) 116 | 117 | #else 118 | 119 | #define PT_REGS_PARM1(x) ((x)->rdi) 120 | #define PT_REGS_PARM2(x) ((x)->rsi) 121 | #define PT_REGS_PARM3(x) ((x)->rdx) 122 | #define PT_REGS_PARM4(x) ((x)->rcx) 123 | #define PT_REGS_PARM5(x) ((x)->r8) 124 | #define PT_REGS_RET(x) ((x)->rsp) 125 | #define PT_REGS_FP(x) ((x)->rbp) 126 | #define PT_REGS_RC(x) ((x)->rax) 127 | #define PT_REGS_SP(x) ((x)->rsp) 128 | #define PT_REGS_IP(x) ((x)->rip) 129 | 130 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((x), rdi) 131 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((x), rsi) 132 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((x), rdx) 133 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((x), rcx) 134 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((x), r8) 135 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((x), rsp) 136 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((x), rbp) 137 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((x), rax) 138 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((x), rsp) 139 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), rip) 140 | 141 | #endif 142 | #endif 143 | 144 | #elif defined(bpf_target_s390) 145 | 146 | /* s390 provides user_pt_regs instead of struct pt_regs to userspace */ 147 | struct pt_regs; 148 | #define PT_REGS_S390 const volatile user_pt_regs 149 | #define PT_REGS_PARM1(x) (((PT_REGS_S390 *)(x))->gprs[2]) 150 | #define PT_REGS_PARM2(x) (((PT_REGS_S390 *)(x))->gprs[3]) 151 | #define PT_REGS_PARM3(x) (((PT_REGS_S390 *)(x))->gprs[4]) 152 | #define PT_REGS_PARM4(x) (((PT_REGS_S390 *)(x))->gprs[5]) 153 | #define PT_REGS_PARM5(x) (((PT_REGS_S390 *)(x))->gprs[6]) 154 | #define PT_REGS_RET(x) (((PT_REGS_S390 *)(x))->gprs[14]) 155 | /* Works only with CONFIG_FRAME_POINTER */ 156 | #define PT_REGS_FP(x) (((PT_REGS_S390 *)(x))->gprs[11]) 157 | #define PT_REGS_RC(x) (((PT_REGS_S390 *)(x))->gprs[2]) 158 | #define PT_REGS_SP(x) (((PT_REGS_S390 *)(x))->gprs[15]) 159 | #define PT_REGS_IP(x) (((PT_REGS_S390 *)(x))->psw.addr) 160 | 161 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[2]) 162 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[3]) 163 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[4]) 164 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[5]) 165 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[6]) 166 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[14]) 167 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[11]) 168 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[2]) 169 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), gprs[15]) 170 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((PT_REGS_S390 *)(x), psw.addr) 171 | 172 | #elif defined(bpf_target_arm) 173 | 174 | #define PT_REGS_PARM1(x) ((x)->uregs[0]) 175 | #define PT_REGS_PARM2(x) ((x)->uregs[1]) 176 | #define PT_REGS_PARM3(x) ((x)->uregs[2]) 177 | #define PT_REGS_PARM4(x) ((x)->uregs[3]) 178 | #define PT_REGS_PARM5(x) ((x)->uregs[4]) 179 | #define PT_REGS_RET(x) ((x)->uregs[14]) 180 | #define PT_REGS_FP(x) ((x)->uregs[11]) /* Works only with CONFIG_FRAME_POINTER */ 181 | #define PT_REGS_RC(x) ((x)->uregs[0]) 182 | #define PT_REGS_SP(x) ((x)->uregs[13]) 183 | #define PT_REGS_IP(x) ((x)->uregs[12]) 184 | 185 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((x), uregs[0]) 186 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((x), uregs[1]) 187 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((x), uregs[2]) 188 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((x), uregs[3]) 189 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((x), uregs[4]) 190 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((x), uregs[14]) 191 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((x), uregs[11]) 192 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((x), uregs[0]) 193 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((x), uregs[13]) 194 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), uregs[12]) 195 | 196 | #elif defined(bpf_target_arm64) 197 | 198 | /* arm64 provides struct user_pt_regs instead of struct pt_regs to userspace */ 199 | struct pt_regs; 200 | #define PT_REGS_ARM64 const volatile struct user_pt_regs 201 | #define PT_REGS_PARM1(x) (((PT_REGS_ARM64 *)(x))->regs[0]) 202 | #define PT_REGS_PARM2(x) (((PT_REGS_ARM64 *)(x))->regs[1]) 203 | #define PT_REGS_PARM3(x) (((PT_REGS_ARM64 *)(x))->regs[2]) 204 | #define PT_REGS_PARM4(x) (((PT_REGS_ARM64 *)(x))->regs[3]) 205 | #define PT_REGS_PARM5(x) (((PT_REGS_ARM64 *)(x))->regs[4]) 206 | #define PT_REGS_RET(x) (((PT_REGS_ARM64 *)(x))->regs[30]) 207 | /* Works only with CONFIG_FRAME_POINTER */ 208 | #define PT_REGS_FP(x) (((PT_REGS_ARM64 *)(x))->regs[29]) 209 | #define PT_REGS_RC(x) (((PT_REGS_ARM64 *)(x))->regs[0]) 210 | #define PT_REGS_SP(x) (((PT_REGS_ARM64 *)(x))->sp) 211 | #define PT_REGS_IP(x) (((PT_REGS_ARM64 *)(x))->pc) 212 | 213 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[0]) 214 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[1]) 215 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[2]) 216 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[3]) 217 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[4]) 218 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[30]) 219 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[29]) 220 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), regs[0]) 221 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), sp) 222 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((PT_REGS_ARM64 *)(x), pc) 223 | 224 | #elif defined(bpf_target_mips) 225 | 226 | #define PT_REGS_PARM1(x) ((x)->regs[4]) 227 | #define PT_REGS_PARM2(x) ((x)->regs[5]) 228 | #define PT_REGS_PARM3(x) ((x)->regs[6]) 229 | #define PT_REGS_PARM4(x) ((x)->regs[7]) 230 | #define PT_REGS_PARM5(x) ((x)->regs[8]) 231 | #define PT_REGS_RET(x) ((x)->regs[31]) 232 | #define PT_REGS_FP(x) ((x)->regs[30]) /* Works only with CONFIG_FRAME_POINTER */ 233 | #define PT_REGS_RC(x) ((x)->regs[2]) 234 | #define PT_REGS_SP(x) ((x)->regs[29]) 235 | #define PT_REGS_IP(x) ((x)->cp0_epc) 236 | 237 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((x), regs[4]) 238 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((x), regs[5]) 239 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((x), regs[6]) 240 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((x), regs[7]) 241 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((x), regs[8]) 242 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((x), regs[31]) 243 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((x), regs[30]) 244 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((x), regs[2]) 245 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((x), regs[29]) 246 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), cp0_epc) 247 | 248 | #elif defined(bpf_target_powerpc) 249 | 250 | #define PT_REGS_PARM1(x) ((x)->gpr[3]) 251 | #define PT_REGS_PARM2(x) ((x)->gpr[4]) 252 | #define PT_REGS_PARM3(x) ((x)->gpr[5]) 253 | #define PT_REGS_PARM4(x) ((x)->gpr[6]) 254 | #define PT_REGS_PARM5(x) ((x)->gpr[7]) 255 | #define PT_REGS_RC(x) ((x)->gpr[3]) 256 | #define PT_REGS_SP(x) ((x)->sp) 257 | #define PT_REGS_IP(x) ((x)->nip) 258 | 259 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((x), gpr[3]) 260 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((x), gpr[4]) 261 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((x), gpr[5]) 262 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((x), gpr[6]) 263 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((x), gpr[7]) 264 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((x), gpr[3]) 265 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((x), sp) 266 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), nip) 267 | 268 | #elif defined(bpf_target_sparc) 269 | 270 | #define PT_REGS_PARM1(x) ((x)->u_regs[UREG_I0]) 271 | #define PT_REGS_PARM2(x) ((x)->u_regs[UREG_I1]) 272 | #define PT_REGS_PARM3(x) ((x)->u_regs[UREG_I2]) 273 | #define PT_REGS_PARM4(x) ((x)->u_regs[UREG_I3]) 274 | #define PT_REGS_PARM5(x) ((x)->u_regs[UREG_I4]) 275 | #define PT_REGS_RET(x) ((x)->u_regs[UREG_I7]) 276 | #define PT_REGS_RC(x) ((x)->u_regs[UREG_I0]) 277 | #define PT_REGS_SP(x) ((x)->u_regs[UREG_FP]) 278 | 279 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((x), u_regs[UREG_I0]) 280 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((x), u_regs[UREG_I1]) 281 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((x), u_regs[UREG_I2]) 282 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((x), u_regs[UREG_I3]) 283 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((x), u_regs[UREG_I4]) 284 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((x), u_regs[UREG_I7]) 285 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((x), u_regs[UREG_I0]) 286 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((x), u_regs[UREG_FP]) 287 | 288 | /* Should this also be a bpf_target check for the sparc case? */ 289 | #if defined(__arch64__) 290 | #define PT_REGS_IP(x) ((x)->tpc) 291 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), tpc) 292 | #else 293 | #define PT_REGS_IP(x) ((x)->pc) 294 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((x), pc) 295 | #endif 296 | 297 | #elif defined(bpf_target_riscv) 298 | 299 | struct pt_regs; 300 | #define PT_REGS_RV const volatile struct user_regs_struct 301 | #define PT_REGS_PARM1(x) (((PT_REGS_RV *)(x))->a0) 302 | #define PT_REGS_PARM2(x) (((PT_REGS_RV *)(x))->a1) 303 | #define PT_REGS_PARM3(x) (((PT_REGS_RV *)(x))->a2) 304 | #define PT_REGS_PARM4(x) (((PT_REGS_RV *)(x))->a3) 305 | #define PT_REGS_PARM5(x) (((PT_REGS_RV *)(x))->a4) 306 | #define PT_REGS_RET(x) (((PT_REGS_RV *)(x))->ra) 307 | #define PT_REGS_FP(x) (((PT_REGS_RV *)(x))->s5) 308 | #define PT_REGS_RC(x) (((PT_REGS_RV *)(x))->a5) 309 | #define PT_REGS_SP(x) (((PT_REGS_RV *)(x))->sp) 310 | #define PT_REGS_IP(x) (((PT_REGS_RV *)(x))->epc) 311 | 312 | #define PT_REGS_PARM1_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), a0) 313 | #define PT_REGS_PARM2_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), a1) 314 | #define PT_REGS_PARM3_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), a2) 315 | #define PT_REGS_PARM4_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), a3) 316 | #define PT_REGS_PARM5_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), a4) 317 | #define PT_REGS_RET_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), ra) 318 | #define PT_REGS_FP_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), fp) 319 | #define PT_REGS_RC_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), a5) 320 | #define PT_REGS_SP_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), sp) 321 | #define PT_REGS_IP_CORE(x) BPF_CORE_READ((PT_REGS_RV *)(x), epc) 322 | 323 | #endif 324 | 325 | #if defined(bpf_target_powerpc) 326 | #define BPF_KPROBE_READ_RET_IP(ip, ctx) ({ (ip) = (ctx)->link; }) 327 | #define BPF_KRETPROBE_READ_RET_IP BPF_KPROBE_READ_RET_IP 328 | #elif defined(bpf_target_sparc) 329 | #define BPF_KPROBE_READ_RET_IP(ip, ctx) ({ (ip) = PT_REGS_RET(ctx); }) 330 | #define BPF_KRETPROBE_READ_RET_IP BPF_KPROBE_READ_RET_IP 331 | #elif defined(bpf_target_defined) 332 | #define BPF_KPROBE_READ_RET_IP(ip, ctx) \ 333 | ({ bpf_probe_read_kernel(&(ip), sizeof(ip), (void *)PT_REGS_RET(ctx)); }) 334 | #define BPF_KRETPROBE_READ_RET_IP(ip, ctx) \ 335 | ({ bpf_probe_read_kernel(&(ip), sizeof(ip), \ 336 | (void *)(PT_REGS_FP(ctx) + sizeof(ip))); }) 337 | #endif 338 | 339 | #if !defined(bpf_target_defined) 340 | 341 | #define PT_REGS_PARM1(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 342 | #define PT_REGS_PARM2(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 343 | #define PT_REGS_PARM3(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 344 | #define PT_REGS_PARM4(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 345 | #define PT_REGS_PARM5(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 346 | #define PT_REGS_RET(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 347 | #define PT_REGS_FP(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 348 | #define PT_REGS_RC(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 349 | #define PT_REGS_SP(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 350 | #define PT_REGS_IP(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 351 | 352 | #define PT_REGS_PARM1_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 353 | #define PT_REGS_PARM2_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 354 | #define PT_REGS_PARM3_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 355 | #define PT_REGS_PARM4_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 356 | #define PT_REGS_PARM5_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 357 | #define PT_REGS_RET_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 358 | #define PT_REGS_FP_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 359 | #define PT_REGS_RC_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 360 | #define PT_REGS_SP_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 361 | #define PT_REGS_IP_CORE(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 362 | 363 | #define BPF_KPROBE_READ_RET_IP(ip, ctx) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 364 | #define BPF_KRETPROBE_READ_RET_IP(ip, ctx) ({ _Pragma(__BPF_TARGET_MISSING); 0l; }) 365 | 366 | #endif /* !defined(bpf_target_defined) */ 367 | 368 | #ifndef ___bpf_concat 369 | #define ___bpf_concat(a, b) a ## b 370 | #endif 371 | #ifndef ___bpf_apply 372 | #define ___bpf_apply(fn, n) ___bpf_concat(fn, n) 373 | #endif 374 | #ifndef ___bpf_nth 375 | #define ___bpf_nth(_, _1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, N, ...) N 376 | #endif 377 | #ifndef ___bpf_narg 378 | #define ___bpf_narg(...) \ 379 | ___bpf_nth(_, ##__VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 380 | #endif 381 | 382 | #define ___bpf_ctx_cast0() ctx 383 | #define ___bpf_ctx_cast1(x) ___bpf_ctx_cast0(), (void *)ctx[0] 384 | #define ___bpf_ctx_cast2(x, args...) ___bpf_ctx_cast1(args), (void *)ctx[1] 385 | #define ___bpf_ctx_cast3(x, args...) ___bpf_ctx_cast2(args), (void *)ctx[2] 386 | #define ___bpf_ctx_cast4(x, args...) ___bpf_ctx_cast3(args), (void *)ctx[3] 387 | #define ___bpf_ctx_cast5(x, args...) ___bpf_ctx_cast4(args), (void *)ctx[4] 388 | #define ___bpf_ctx_cast6(x, args...) ___bpf_ctx_cast5(args), (void *)ctx[5] 389 | #define ___bpf_ctx_cast7(x, args...) ___bpf_ctx_cast6(args), (void *)ctx[6] 390 | #define ___bpf_ctx_cast8(x, args...) ___bpf_ctx_cast7(args), (void *)ctx[7] 391 | #define ___bpf_ctx_cast9(x, args...) ___bpf_ctx_cast8(args), (void *)ctx[8] 392 | #define ___bpf_ctx_cast10(x, args...) ___bpf_ctx_cast9(args), (void *)ctx[9] 393 | #define ___bpf_ctx_cast11(x, args...) ___bpf_ctx_cast10(args), (void *)ctx[10] 394 | #define ___bpf_ctx_cast12(x, args...) ___bpf_ctx_cast11(args), (void *)ctx[11] 395 | #define ___bpf_ctx_cast(args...) \ 396 | ___bpf_apply(___bpf_ctx_cast, ___bpf_narg(args))(args) 397 | 398 | /* 399 | * BPF_PROG is a convenience wrapper for generic tp_btf/fentry/fexit and 400 | * similar kinds of BPF programs, that accept input arguments as a single 401 | * pointer to untyped u64 array, where each u64 can actually be a typed 402 | * pointer or integer of different size. Instead of requring user to write 403 | * manual casts and work with array elements by index, BPF_PROG macro 404 | * allows user to declare a list of named and typed input arguments in the 405 | * same syntax as for normal C function. All the casting is hidden and 406 | * performed transparently, while user code can just assume working with 407 | * function arguments of specified type and name. 408 | * 409 | * Original raw context argument is preserved as well as 'ctx' argument. 410 | * This is useful when using BPF helpers that expect original context 411 | * as one of the parameters (e.g., for bpf_perf_event_output()). 412 | */ 413 | #define BPF_PROG(name, args...) \ 414 | name(unsigned long long *ctx); \ 415 | static __attribute__((always_inline)) typeof(name(0)) \ 416 | ____##name(unsigned long long *ctx, ##args); \ 417 | typeof(name(0)) name(unsigned long long *ctx) \ 418 | { \ 419 | _Pragma("GCC diagnostic push") \ 420 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 421 | return ____##name(___bpf_ctx_cast(args)); \ 422 | _Pragma("GCC diagnostic pop") \ 423 | } \ 424 | static __attribute__((always_inline)) typeof(name(0)) \ 425 | ____##name(unsigned long long *ctx, ##args) 426 | 427 | struct pt_regs; 428 | 429 | #define ___bpf_kprobe_args0() ctx 430 | #define ___bpf_kprobe_args1(x) \ 431 | ___bpf_kprobe_args0(), (void *)PT_REGS_PARM1(ctx) 432 | #define ___bpf_kprobe_args2(x, args...) \ 433 | ___bpf_kprobe_args1(args), (void *)PT_REGS_PARM2(ctx) 434 | #define ___bpf_kprobe_args3(x, args...) \ 435 | ___bpf_kprobe_args2(args), (void *)PT_REGS_PARM3(ctx) 436 | #define ___bpf_kprobe_args4(x, args...) \ 437 | ___bpf_kprobe_args3(args), (void *)PT_REGS_PARM4(ctx) 438 | #define ___bpf_kprobe_args5(x, args...) \ 439 | ___bpf_kprobe_args4(args), (void *)PT_REGS_PARM5(ctx) 440 | #define ___bpf_kprobe_args(args...) \ 441 | ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args) 442 | 443 | /* 444 | * BPF_KPROBE serves the same purpose for kprobes as BPF_PROG for 445 | * tp_btf/fentry/fexit BPF programs. It hides the underlying platform-specific 446 | * low-level way of getting kprobe input arguments from struct pt_regs, and 447 | * provides a familiar typed and named function arguments syntax and 448 | * semantics of accessing kprobe input paremeters. 449 | * 450 | * Original struct pt_regs* context is preserved as 'ctx' argument. This might 451 | * be necessary when using BPF helpers like bpf_perf_event_output(). 452 | */ 453 | #define BPF_KPROBE(name, args...) \ 454 | name(struct pt_regs *ctx); \ 455 | static __attribute__((always_inline)) typeof(name(0)) \ 456 | ____##name(struct pt_regs *ctx, ##args); \ 457 | typeof(name(0)) name(struct pt_regs *ctx) \ 458 | { \ 459 | _Pragma("GCC diagnostic push") \ 460 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 461 | return ____##name(___bpf_kprobe_args(args)); \ 462 | _Pragma("GCC diagnostic pop") \ 463 | } \ 464 | static __attribute__((always_inline)) typeof(name(0)) \ 465 | ____##name(struct pt_regs *ctx, ##args) 466 | 467 | #define ___bpf_kretprobe_args0() ctx 468 | #define ___bpf_kretprobe_args1(x) \ 469 | ___bpf_kretprobe_args0(), (void *)PT_REGS_RC(ctx) 470 | #define ___bpf_kretprobe_args(args...) \ 471 | ___bpf_apply(___bpf_kretprobe_args, ___bpf_narg(args))(args) 472 | 473 | /* 474 | * BPF_KRETPROBE is similar to BPF_KPROBE, except, it only provides optional 475 | * return value (in addition to `struct pt_regs *ctx`), but no input 476 | * arguments, because they will be clobbered by the time probed function 477 | * returns. 478 | */ 479 | #define BPF_KRETPROBE(name, args...) \ 480 | name(struct pt_regs *ctx); \ 481 | static __attribute__((always_inline)) typeof(name(0)) \ 482 | ____##name(struct pt_regs *ctx, ##args); \ 483 | typeof(name(0)) name(struct pt_regs *ctx) \ 484 | { \ 485 | _Pragma("GCC diagnostic push") \ 486 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 487 | return ____##name(___bpf_kretprobe_args(args)); \ 488 | _Pragma("GCC diagnostic pop") \ 489 | } \ 490 | static __always_inline typeof(name(0)) ____##name(struct pt_regs *ctx, ##args) 491 | 492 | #endif 493 | -------------------------------------------------------------------------------- /include/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Version of libbpf to fetch headers from 4 | LIBBPF_VERSION=0.6.1 5 | 6 | # The headers we want 7 | prefix=libbpf-"$LIBBPF_VERSION" 8 | headers=( 9 | "$prefix"/LICENSE.BSD-2-Clause 10 | "$prefix"/src/bpf_endian.h 11 | "$prefix"/src/bpf_helper_defs.h 12 | "$prefix"/src/bpf_helpers.h 13 | "$prefix"/src/bpf_tracing.h 14 | ) 15 | 16 | # Fetch libbpf release and extract the desired headers 17 | curl -sL "https://github.com/libbpf/libbpf/archive/refs/tags/v${LIBBPF_VERSION}.tar.gz" | \ 18 | tar -xz --xform='s#.*/##' "${headers[@]}" 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build linux 15 | 16 | package main 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "fmt" 22 | "os" 23 | "runtime" 24 | "strconv" 25 | "strings" 26 | "sync" 27 | "time" 28 | 29 | "github.com/parca-dev/oomprof/oomprof" 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | func main() { 34 | // Enable memory profiling for this process 35 | runtime.MemProfile(nil, false) 36 | 37 | var pidsFlag string 38 | var debug bool 39 | flag.StringVar(&pidsFlag, "p", "", "Comma-delimited list of PIDs to profile") 40 | flag.BoolVar(&debug, "debug", false, "Enable debug logging") 41 | flag.Parse() 42 | 43 | // Configure logging 44 | if debug { 45 | log.SetLevel(log.DebugLevel) 46 | } else { 47 | log.SetLevel(log.InfoLevel) 48 | } 49 | 50 | ctx := context.Background() 51 | 52 | // Create channel for profile data with buffer to avoid blocking 53 | profileChan := make(chan oomprof.ProfileData, 10) 54 | 55 | cfg := oomprof.Config{ 56 | MemLimit: 32, 57 | Verbose: true, 58 | Symbolize: true, 59 | LogTracePipe: debug, 60 | } 61 | state, err := oomprof.Setup(ctx, &cfg, profileChan) 62 | if err != nil { 63 | fmt.Fprintf(os.Stderr, "Failed to setup OOM profiler: %v\n", err) 64 | os.Exit(1) 65 | } 66 | defer state.Close() 67 | 68 | // Start goroutine to write profiles to disk 69 | var wg sync.WaitGroup 70 | wg.Add(1) 71 | go func() { 72 | defer wg.Done() 73 | log.Debug("Profile writer goroutine started") 74 | for profile := range profileChan { 75 | log.WithFields(log.Fields{"pid": profile.PID, "command": profile.Command}).Debug("Received profile") 76 | 77 | // Skip empty profiles 78 | if profile.Profile == nil || len(profile.Profile.Sample) == 0 { 79 | log.WithField("pid", profile.PID).Debug("Skipping empty profile") 80 | continue 81 | } 82 | 83 | // Create filename with timestamp: command-pid-YYYYMMDDHHmmss.pb.gz 84 | timestamp := time.Now().Format("20060102150405") 85 | filename := fmt.Sprintf("%s-%d-%s.pb.gz", profile.Command, profile.PID, timestamp) 86 | f, err := os.Create(filename) 87 | if err != nil { 88 | log.WithError(err).WithField("filename", filename).Error("Failed to create profile file") 89 | continue 90 | } 91 | 92 | if err := profile.Profile.Write(f); err != nil { 93 | log.WithError(err).WithField("filename", filename).Error("Failed to write profile") 94 | } 95 | f.Close() 96 | log.WithField("filename", filename).Info("Profile written") 97 | } 98 | log.Debug("Profile writer goroutine exiting") 99 | }() 100 | 101 | // If -p flag is provided, profile specific PIDs 102 | if pidsFlag != "" { 103 | pids := strings.Split(pidsFlag, ",") 104 | for _, pidStr := range pids { 105 | pidStr = strings.TrimSpace(pidStr) 106 | var pid int 107 | 108 | if pidStr == "self" { 109 | // Profile the current process (oompa itself) 110 | pid = os.Getpid() 111 | log.WithField("pid", pid).Debug("Profiling self") 112 | } else { 113 | var err error 114 | pid, err = strconv.Atoi(pidStr) 115 | if err != nil { 116 | log.WithField("pid", pidStr).Error("Invalid PID") 117 | continue 118 | } 119 | log.WithField("pid", pid).Debug("Profiling PID") 120 | } 121 | 122 | if err := state.ProfilePid(ctx, uint32(pid)); err != nil { 123 | log.WithError(err).WithField("pid", pid).Error("Failed to profile PID") 124 | } 125 | } 126 | // Close the channel after all PIDs are profiled 127 | close(profileChan) 128 | // Wait for all profiles to be written 129 | wg.Wait() 130 | } else { 131 | // For OOM monitoring mode, keep running until interrupted 132 | wg.Wait() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /oomprof.c: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | //go:build ignore 3 | // clang-format on 4 | 5 | // Copyright 2022-2025 The Parca Authors 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | // Produced by: 19 | // bpftool btf dump file /sys/kernel/btf/vmlinux format c > include/vmlinux.h 20 | #include "include/vmlinux.h" 21 | 22 | // ARM64 compatibility definitions 23 | #include "include/arm64_compat.h" 24 | 25 | #include "include/bpf_core_read.h" 26 | #include "include/bpf_helpers.h" 27 | #include "include/bpf_tracing.h" 28 | 29 | // Override bpf_printk to always use bpf_trace_printk for compatibility with older kernels 30 | // FIXME: regenerate headers using a 5.4 kernel 31 | #undef bpf_printk 32 | #define bpf_printk(fmt, args...) \ 33 | ({ \ 34 | char ____fmt[] = fmt; \ 35 | bpf_trace_printk(____fmt, sizeof(____fmt), ##args); \ 36 | }) 37 | 38 | // Conditional debug logging macro - for now just use bpf_printk directly 39 | // TODO: Make this runtime configurable without verifier issues 40 | #define DEBUG_PRINT(fmt, args...) bpf_printk(fmt, ##args) 41 | 42 | #define MAX_STACK_DEPTH 64 43 | #define MAX_BUCKETS_PER_CALL 3362 44 | // Maximum number of tail calls allowed. With MAX_BUCKETS_PER_LOOP=2729, this allows 45 | // processing up to (10+1) * 2729 = 30,019 buckets, which fits within the 30,000 46 | // mem_buckets map limit. If this value is increased, mem_buckets max_entries must 47 | // also be increased proportionally: new_limit >= (MAX_TAIL_CALLS+1) * MAX_BUCKETS_PER_LOOP 48 | #define MAX_TAIL_CALLS 30 49 | 50 | char __license[] SEC("license") = "Dual MIT/GPL"; 51 | 52 | typedef enum Programs { 53 | RECORD_PROFILE_BUCKETS_PROG=0 54 | } Programs; 55 | 56 | // https://github.com/golang/go/blob/6885bad7dd/src/runtime/mprof.go#L148 57 | struct memRecordCycle { 58 | u64 allocs; 59 | u64 frees; 60 | u64 allocBytes; 61 | u64 freeBytes; 62 | }; 63 | 64 | // https://github.com/golang/go/blob/6885bad7dd/src/runtime/mprof.go#L87 65 | struct memRecord { 66 | struct memRecordCycle active; 67 | struct memRecordCycle future[3]; 68 | }; 69 | 70 | // https://github.com/golang/go/blob/6885bad7dd/src/runtime/mprof.go#L75 71 | struct gobucket_header { 72 | u64 next; 73 | u64 allnext; 74 | u64 bucketType; // memBucket or blockBucket (includes mutexProfile) 75 | u64 hash; 76 | u64 size; 77 | u64 nstk; 78 | }; 79 | 80 | // This isn't the real Go structure, we have a fixed stack and there's 81 | // is variable, we calculate the address of the memRecord in Go. 82 | struct gobucket { 83 | struct gobucket_header header; 84 | u64 stk[MAX_STACK_DEPTH]; 85 | struct memRecord mem; 86 | }; 87 | 88 | 89 | struct GoProc { 90 | u64 mbuckets; // static doesn't change 91 | u32 num_buckets; // updated after profile is recorded 92 | u32 maxStackErrors; 93 | bool readError; 94 | bool complete; 95 | bool reportAlloc; // whether to report alloc metrics or just inuse 96 | bool started; // whether the process has started profiling 97 | }; 98 | 99 | struct Event { 100 | u32 event_type; 101 | u32 payload; 102 | }; 103 | 104 | struct ProfileState { 105 | u32 pid; 106 | u64 gobp; 107 | u32 bucket_count; 108 | u32 num_tail_calls; // used to limit tail calls 109 | }; 110 | 111 | 112 | // Tail call map for bucket processing - support up to 10 tail calls 113 | struct { 114 | __uint(type, BPF_MAP_TYPE_PROG_ARRAY); 115 | __uint(max_entries, 1); 116 | __type(key, u32); 117 | __type(value, u32); 118 | } tail_call_map SEC(".maps"); 119 | 120 | // State for tail call bucket processing 121 | struct { 122 | __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); 123 | __type(key, int); 124 | __type(value, struct ProfileState); 125 | __uint(max_entries, 1); 126 | } profile_state SEC(".maps"); 127 | 128 | #define MAX_BUCKETS 60000 129 | 130 | // Default to handle large real programs with many allocation sites 131 | struct { 132 | __uint(type, BPF_MAP_TYPE_ARRAY); 133 | __type(key, u32); 134 | __type(value, struct gobucket); 135 | __uint(max_entries, MAX_BUCKETS); 136 | } mem_buckets SEC(".maps"); 137 | 138 | // Map of go procs to mbuckets address 139 | struct { 140 | __uint(type, BPF_MAP_TYPE_HASH); 141 | __type(key, u32); 142 | __type(value, struct GoProc); 143 | __uint(max_entries, 1024); 144 | } go_procs SEC(".maps"); 145 | 146 | // Global entry for the pid of the process we are profiling, key always 0 147 | struct { 148 | __uint(type, BPF_MAP_TYPE_HASH); 149 | __type(key, u32); 150 | __type(value, pid_t); 151 | __uint(max_entries, 1); 152 | } profile_pid SEC(".maps"); 153 | 154 | 155 | struct { 156 | __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); 157 | __type(key, u32); 158 | //__type(value, u32); // Should be u32 (file descriptor) 159 | //__uint(max_entries, 1024); 160 | //__type(value, struct Event); 161 | } signal_events SEC(".maps"); 162 | 163 | // Dummy map just to export the struct - never actually used 164 | struct { 165 | __uint(type, BPF_MAP_TYPE_HASH); 166 | __type(key, u32); 167 | __type(value, struct Event); // This forces BTF generation 168 | __uint(max_entries, 1); 169 | } dummy_event_map SEC(".maps"); 170 | 171 | // Dummy map just to export the struct - never actually used 172 | struct { 173 | __uint(type, BPF_MAP_TYPE_HASH); 174 | __type(key, u32); 175 | __type(value, struct memRecord); // This forces BTF generation 176 | __uint(max_entries, 1); 177 | } dummy_record_map SEC(".maps"); 178 | 179 | static inline __attribute__((__always_inline__)) int 180 | record_profile_buckets(void *ctx, struct ProfileState *state) { 181 | DEBUG_PRINT("recording profile buckets for pid: %d mbuckets:%llx buckets:%d\n", state->pid, state->gobp,state->bucket_count); 182 | struct gobucket *mbp = 0; 183 | pid_t pid = state->pid; 184 | int err = 0; 185 | struct GoProc* gop = bpf_map_lookup_elem(&go_procs, &pid); 186 | if (!gop) { 187 | DEBUG_PRINT("signal_probe: go_procs lookup failed: %d\n", pid); 188 | return 0; 189 | } 190 | 191 | for (u32 i=0; state->gobp != 0; i++, state->gobp = mbp->header.allnext) { 192 | if (i >= MAX_BUCKETS_PER_CALL) { 193 | state->num_tail_calls++; 194 | if (state->num_tail_calls > MAX_TAIL_CALLS) { 195 | DEBUG_PRINT("record_profile_buckets: too many tail calls, aborting\n"); 196 | return 0; 197 | } 198 | bpf_tail_call(ctx, &tail_call_map, RECORD_PROFILE_BUCKETS_PROG); 199 | return 0; 200 | } 201 | 202 | int key = state->bucket_count; 203 | mbp = bpf_map_lookup_elem(&mem_buckets, &key); 204 | if (!mbp) { 205 | DEBUG_PRINT("mem_buckets lookup failed %d:%llx\n", i, state->gobp); 206 | return 0; 207 | } 208 | 209 | // read header first 210 | if ((err = bpf_probe_read_user(&mbp->header, sizeof(struct gobucket_header), (void*)state->gobp))) { 211 | DEBUG_PRINT("failed to read bucket header at %llx err: %d\n", state->gobp, err); 212 | gop->readError = true; 213 | break; 214 | } 215 | // read stack next 216 | u64 stack_size = mbp->header.nstk * sizeof(u64); 217 | if (stack_size > (MAX_STACK_DEPTH * sizeof(u64))) { 218 | DEBUG_PRINT("skipping bucket %d: nstk=%llu > MAX=%d\n ", i, mbp->header.nstk, MAX_STACK_DEPTH); 219 | continue; 220 | } 221 | if ((err = bpf_probe_read_user(&mbp->stk, stack_size, (void*)(state->gobp + sizeof(struct gobucket_header))))) { 222 | DEBUG_PRINT("failed to read bucket stack at %llx err: %d\n", state->gobp + sizeof(struct gobucket_header), err); 223 | gop->readError = true; 224 | break; 225 | } 226 | // read memRecord last 227 | if ((err = bpf_probe_read_user(&mbp->mem, sizeof(struct memRecord), (void*)(state->gobp + sizeof(struct gobucket_header) + stack_size)))) { 228 | DEBUG_PRINT("failed to read bucket memRecord at %llx err: %d\n", state->gobp + sizeof(struct gobucket_header) + stack_size, err); 229 | gop->readError = true; 230 | break; 231 | } 232 | 233 | state->bucket_count++; 234 | // Need this to appease the verifier and allow the 235 | if (state->bucket_count >= MAX_BUCKETS) { 236 | DEBUG_PRINT("record_profile_buckets: bucket count exceeded max, aborting\n"); 237 | return 0; 238 | } 239 | } 240 | 241 | if (state->gobp == 0) { 242 | gop->complete = true; 243 | } 244 | 245 | DEBUG_PRINT("found %d gobuckets\n", state->bucket_count); 246 | gop->num_buckets = state->bucket_count; 247 | 248 | // Signal userland 249 | DEBUG_PRINT("sending profile recorded event: %d\n", state->pid); 250 | struct Event ev = { .event_type = 1, .payload = state->pid}; 251 | bpf_perf_event_output(ctx, &signal_events, BPF_F_CURRENT_CPU, 252 | &ev, sizeof(struct Event)); 253 | 254 | return 0; 255 | } 256 | 257 | SEC("tracepoint/record_profile_buckets_tail_call") 258 | int record_profile_buckets_prog(void *ctx) { 259 | int key=0; 260 | struct ProfileState *state = bpf_map_lookup_elem(&profile_state, &key); 261 | if (!state) { 262 | return 0; 263 | } 264 | return record_profile_buckets(ctx, state); 265 | } 266 | 267 | // don't rely on BTF 268 | struct _trace_event_raw_mark_victim { 269 | __u64 unused; 270 | int pid; 271 | }; 272 | 273 | SEC("tracepoint/oom/mark_victim") 274 | int oom_mark_victim_handler(struct _trace_event_raw_mark_victim *args) { 275 | pid_t victim_pid = args->pid; 276 | DEBUG_PRINT("oom_mark_victim_handler: victim pid: %d\n", victim_pid); 277 | 278 | int key=0; 279 | pid_t *target_pid = bpf_map_lookup_elem(&profile_pid, &key); 280 | if (!target_pid) { 281 | DEBUG_PRINT("profile_pid lookup failed\n"); 282 | return 0; 283 | } 284 | 285 | if (*target_pid != 0) { 286 | DEBUG_PRINT("profile_pid already set to %d, ignoring new victim pid %d\n", 287 | *target_pid, victim_pid); 288 | return 0; 289 | } 290 | 291 | struct GoProc* gop = bpf_map_lookup_elem(&go_procs, &victim_pid); 292 | if (!gop) { 293 | // If we don't know about him we can't profile him. 294 | DEBUG_PRINT("oom_mark_victim: go_procs lookup failed, ignoring %d\n", victim_pid); 295 | return 0; 296 | } 297 | 298 | // Set the target PID in the profile_pid map so signal_probe can read it 299 | // so we can distinguish true oom kills from non-oom kills. 300 | *target_pid = victim_pid; 301 | 302 | return 0; 303 | } 304 | 305 | SEC("tracepoint/signal/signal_deliver") 306 | int signal_probe(struct trace_event_raw_signal_deliver *ctx) { 307 | pid_t pid = bpf_get_current_pid_tgid() >> 32; 308 | struct GoProc* gop = bpf_map_lookup_elem(&go_procs, &pid); 309 | if (!gop) { 310 | return 0; 311 | } 312 | //DEBUG_PRINT("signal_probe: go_procs lookup succeeded: %d\n", pid); 313 | 314 | int key=0; 315 | pid_t *target_pid = bpf_map_lookup_elem(&profile_pid, &key); 316 | if (!target_pid) { 317 | DEBUG_PRINT("profile_pid lookup failed\n"); 318 | return 0; 319 | } 320 | if (*target_pid != pid) { 321 | return 0; 322 | } 323 | DEBUG_PRINT("signal_probe: target pid == current pid, proceeding\n"); 324 | // num_buckets reset after reading, ignore if not zero. 325 | if (gop->started) { 326 | DEBUG_PRINT("signal_probe: already recorded profile for pid %d, ignoring signal\n", pid); 327 | return 0; 328 | } 329 | gop->started = true; 330 | DEBUG_PRINT("go proc %d got signal\n", pid); 331 | 332 | struct ProfileState *state = bpf_map_lookup_elem(&profile_state, &key); 333 | if (!state) { 334 | return 0; 335 | } 336 | state->pid = pid; 337 | state->bucket_count = 0; 338 | state->num_tail_calls = 0; 339 | u64 gobp; 340 | if (bpf_probe_read_user(&gobp, sizeof(void*), (void*)gop->mbuckets)) { 341 | DEBUG_PRINT("failed to read mbuckets pointer for pid: %d\n", pid); 342 | return 0; 343 | } 344 | state->gobp = gobp; 345 | record_profile_buckets(ctx, state); 346 | 347 | return 0; 348 | } 349 | -------------------------------------------------------------------------------- /oomprof/bpf_arm64_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build arm64 && linux 3 | 4 | package oomprof 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | "structs" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type bpfEvent struct { 17 | _ structs.HostLayout 18 | EventType uint32 19 | Payload uint32 20 | } 21 | 22 | type bpfGoProc struct { 23 | _ structs.HostLayout 24 | Mbuckets uint64 25 | NumBuckets uint32 26 | MaxStackErrors uint32 27 | ReadError bool 28 | Complete bool 29 | ReportAlloc bool 30 | Started bool 31 | _ [4]byte 32 | } 33 | 34 | type bpfGobucket struct { 35 | _ structs.HostLayout 36 | Header struct { 37 | _ structs.HostLayout 38 | Next uint64 39 | Allnext uint64 40 | BucketType uint64 41 | Hash uint64 42 | Size uint64 43 | Nstk uint64 44 | } 45 | Stk [64]uint64 46 | Mem bpfMemRecord 47 | } 48 | 49 | type bpfMemRecord struct { 50 | _ structs.HostLayout 51 | Active struct { 52 | _ structs.HostLayout 53 | Allocs uint64 54 | Frees uint64 55 | AllocBytes uint64 56 | FreeBytes uint64 57 | } 58 | Future [3]struct { 59 | _ structs.HostLayout 60 | Allocs uint64 61 | Frees uint64 62 | AllocBytes uint64 63 | FreeBytes uint64 64 | } 65 | } 66 | 67 | type bpfProfileState struct { 68 | _ structs.HostLayout 69 | Pid uint32 70 | _ [4]byte 71 | Gobp uint64 72 | BucketCount uint32 73 | NumTailCalls uint32 74 | } 75 | 76 | // loadBpf returns the embedded CollectionSpec for bpf. 77 | func loadBpf() (*ebpf.CollectionSpec, error) { 78 | reader := bytes.NewReader(_BpfBytes) 79 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 80 | if err != nil { 81 | return nil, fmt.Errorf("can't load bpf: %w", err) 82 | } 83 | 84 | return spec, err 85 | } 86 | 87 | // loadBpfObjects loads bpf and converts it into a struct. 88 | // 89 | // The following types are suitable as obj argument: 90 | // 91 | // *bpfObjects 92 | // *bpfPrograms 93 | // *bpfMaps 94 | // 95 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 96 | func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 97 | spec, err := loadBpf() 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return spec.LoadAndAssign(obj, opts) 103 | } 104 | 105 | // bpfSpecs contains maps and programs before they are loaded into the kernel. 106 | // 107 | // It can be passed ebpf.CollectionSpec.Assign. 108 | type bpfSpecs struct { 109 | bpfProgramSpecs 110 | bpfMapSpecs 111 | bpfVariableSpecs 112 | } 113 | 114 | // bpfProgramSpecs contains programs before they are loaded into the kernel. 115 | // 116 | // It can be passed ebpf.CollectionSpec.Assign. 117 | type bpfProgramSpecs struct { 118 | OomMarkVictimHandler *ebpf.ProgramSpec `ebpf:"oom_mark_victim_handler"` 119 | RecordProfileBucketsProg *ebpf.ProgramSpec `ebpf:"record_profile_buckets_prog"` 120 | SignalProbe *ebpf.ProgramSpec `ebpf:"signal_probe"` 121 | } 122 | 123 | // bpfMapSpecs contains maps before they are loaded into the kernel. 124 | // 125 | // It can be passed ebpf.CollectionSpec.Assign. 126 | type bpfMapSpecs struct { 127 | DummyEventMap *ebpf.MapSpec `ebpf:"dummy_event_map"` 128 | DummyRecordMap *ebpf.MapSpec `ebpf:"dummy_record_map"` 129 | GoProcs *ebpf.MapSpec `ebpf:"go_procs"` 130 | MemBuckets *ebpf.MapSpec `ebpf:"mem_buckets"` 131 | ProfilePid *ebpf.MapSpec `ebpf:"profile_pid"` 132 | ProfileState *ebpf.MapSpec `ebpf:"profile_state"` 133 | SignalEvents *ebpf.MapSpec `ebpf:"signal_events"` 134 | TailCallMap *ebpf.MapSpec `ebpf:"tail_call_map"` 135 | } 136 | 137 | // bpfVariableSpecs contains global variables before they are loaded into the kernel. 138 | // 139 | // It can be passed ebpf.CollectionSpec.Assign. 140 | type bpfVariableSpecs struct { 141 | } 142 | 143 | // bpfObjects contains all objects after they have been loaded into the kernel. 144 | // 145 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 146 | type bpfObjects struct { 147 | bpfPrograms 148 | bpfMaps 149 | bpfVariables 150 | } 151 | 152 | func (o *bpfObjects) Close() error { 153 | return _BpfClose( 154 | &o.bpfPrograms, 155 | &o.bpfMaps, 156 | ) 157 | } 158 | 159 | // bpfMaps contains all maps after they have been loaded into the kernel. 160 | // 161 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 162 | type bpfMaps struct { 163 | DummyEventMap *ebpf.Map `ebpf:"dummy_event_map"` 164 | DummyRecordMap *ebpf.Map `ebpf:"dummy_record_map"` 165 | GoProcs *ebpf.Map `ebpf:"go_procs"` 166 | MemBuckets *ebpf.Map `ebpf:"mem_buckets"` 167 | ProfilePid *ebpf.Map `ebpf:"profile_pid"` 168 | ProfileState *ebpf.Map `ebpf:"profile_state"` 169 | SignalEvents *ebpf.Map `ebpf:"signal_events"` 170 | TailCallMap *ebpf.Map `ebpf:"tail_call_map"` 171 | } 172 | 173 | func (m *bpfMaps) Close() error { 174 | return _BpfClose( 175 | m.DummyEventMap, 176 | m.DummyRecordMap, 177 | m.GoProcs, 178 | m.MemBuckets, 179 | m.ProfilePid, 180 | m.ProfileState, 181 | m.SignalEvents, 182 | m.TailCallMap, 183 | ) 184 | } 185 | 186 | // bpfVariables contains all global variables after they have been loaded into the kernel. 187 | // 188 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 189 | type bpfVariables struct { 190 | } 191 | 192 | // bpfPrograms contains all programs after they have been loaded into the kernel. 193 | // 194 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 195 | type bpfPrograms struct { 196 | OomMarkVictimHandler *ebpf.Program `ebpf:"oom_mark_victim_handler"` 197 | RecordProfileBucketsProg *ebpf.Program `ebpf:"record_profile_buckets_prog"` 198 | SignalProbe *ebpf.Program `ebpf:"signal_probe"` 199 | } 200 | 201 | func (p *bpfPrograms) Close() error { 202 | return _BpfClose( 203 | p.OomMarkVictimHandler, 204 | p.RecordProfileBucketsProg, 205 | p.SignalProbe, 206 | ) 207 | } 208 | 209 | func _BpfClose(closers ...io.Closer) error { 210 | for _, closer := range closers { 211 | if err := closer.Close(); err != nil { 212 | return err 213 | } 214 | } 215 | return nil 216 | } 217 | 218 | // Do not access this directly. 219 | // 220 | //go:embed bpf_arm64_bpfel.o 221 | var _BpfBytes []byte 222 | -------------------------------------------------------------------------------- /oomprof/bpf_arm64_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parca-dev/oomprof/ec00408377fba70a12cada4e0c2fca5b04314ce7/oomprof/bpf_arm64_bpfel.o -------------------------------------------------------------------------------- /oomprof/bpf_x86_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build (386 || amd64) && linux 3 | 4 | package oomprof 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | "structs" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type bpfEvent struct { 17 | _ structs.HostLayout 18 | EventType uint32 19 | Payload uint32 20 | } 21 | 22 | type bpfGoProc struct { 23 | _ structs.HostLayout 24 | Mbuckets uint64 25 | NumBuckets uint32 26 | MaxStackErrors uint32 27 | ReadError bool 28 | Complete bool 29 | ReportAlloc bool 30 | Started bool 31 | _ [4]byte 32 | } 33 | 34 | type bpfGobucket struct { 35 | _ structs.HostLayout 36 | Header struct { 37 | _ structs.HostLayout 38 | Next uint64 39 | Allnext uint64 40 | BucketType uint64 41 | Hash uint64 42 | Size uint64 43 | Nstk uint64 44 | } 45 | Stk [64]uint64 46 | Mem bpfMemRecord 47 | } 48 | 49 | type bpfMemRecord struct { 50 | _ structs.HostLayout 51 | Active struct { 52 | _ structs.HostLayout 53 | Allocs uint64 54 | Frees uint64 55 | AllocBytes uint64 56 | FreeBytes uint64 57 | } 58 | Future [3]struct { 59 | _ structs.HostLayout 60 | Allocs uint64 61 | Frees uint64 62 | AllocBytes uint64 63 | FreeBytes uint64 64 | } 65 | } 66 | 67 | type bpfProfileState struct { 68 | _ structs.HostLayout 69 | Pid uint32 70 | _ [4]byte 71 | Gobp uint64 72 | BucketCount uint32 73 | NumTailCalls uint32 74 | } 75 | 76 | // loadBpf returns the embedded CollectionSpec for bpf. 77 | func loadBpf() (*ebpf.CollectionSpec, error) { 78 | reader := bytes.NewReader(_BpfBytes) 79 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 80 | if err != nil { 81 | return nil, fmt.Errorf("can't load bpf: %w", err) 82 | } 83 | 84 | return spec, err 85 | } 86 | 87 | // loadBpfObjects loads bpf and converts it into a struct. 88 | // 89 | // The following types are suitable as obj argument: 90 | // 91 | // *bpfObjects 92 | // *bpfPrograms 93 | // *bpfMaps 94 | // 95 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 96 | func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 97 | spec, err := loadBpf() 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return spec.LoadAndAssign(obj, opts) 103 | } 104 | 105 | // bpfSpecs contains maps and programs before they are loaded into the kernel. 106 | // 107 | // It can be passed ebpf.CollectionSpec.Assign. 108 | type bpfSpecs struct { 109 | bpfProgramSpecs 110 | bpfMapSpecs 111 | bpfVariableSpecs 112 | } 113 | 114 | // bpfProgramSpecs contains programs before they are loaded into the kernel. 115 | // 116 | // It can be passed ebpf.CollectionSpec.Assign. 117 | type bpfProgramSpecs struct { 118 | OomMarkVictimHandler *ebpf.ProgramSpec `ebpf:"oom_mark_victim_handler"` 119 | RecordProfileBucketsProg *ebpf.ProgramSpec `ebpf:"record_profile_buckets_prog"` 120 | SignalProbe *ebpf.ProgramSpec `ebpf:"signal_probe"` 121 | } 122 | 123 | // bpfMapSpecs contains maps before they are loaded into the kernel. 124 | // 125 | // It can be passed ebpf.CollectionSpec.Assign. 126 | type bpfMapSpecs struct { 127 | DummyEventMap *ebpf.MapSpec `ebpf:"dummy_event_map"` 128 | DummyRecordMap *ebpf.MapSpec `ebpf:"dummy_record_map"` 129 | GoProcs *ebpf.MapSpec `ebpf:"go_procs"` 130 | MemBuckets *ebpf.MapSpec `ebpf:"mem_buckets"` 131 | ProfilePid *ebpf.MapSpec `ebpf:"profile_pid"` 132 | ProfileState *ebpf.MapSpec `ebpf:"profile_state"` 133 | SignalEvents *ebpf.MapSpec `ebpf:"signal_events"` 134 | TailCallMap *ebpf.MapSpec `ebpf:"tail_call_map"` 135 | } 136 | 137 | // bpfVariableSpecs contains global variables before they are loaded into the kernel. 138 | // 139 | // It can be passed ebpf.CollectionSpec.Assign. 140 | type bpfVariableSpecs struct { 141 | } 142 | 143 | // bpfObjects contains all objects after they have been loaded into the kernel. 144 | // 145 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 146 | type bpfObjects struct { 147 | bpfPrograms 148 | bpfMaps 149 | bpfVariables 150 | } 151 | 152 | func (o *bpfObjects) Close() error { 153 | return _BpfClose( 154 | &o.bpfPrograms, 155 | &o.bpfMaps, 156 | ) 157 | } 158 | 159 | // bpfMaps contains all maps after they have been loaded into the kernel. 160 | // 161 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 162 | type bpfMaps struct { 163 | DummyEventMap *ebpf.Map `ebpf:"dummy_event_map"` 164 | DummyRecordMap *ebpf.Map `ebpf:"dummy_record_map"` 165 | GoProcs *ebpf.Map `ebpf:"go_procs"` 166 | MemBuckets *ebpf.Map `ebpf:"mem_buckets"` 167 | ProfilePid *ebpf.Map `ebpf:"profile_pid"` 168 | ProfileState *ebpf.Map `ebpf:"profile_state"` 169 | SignalEvents *ebpf.Map `ebpf:"signal_events"` 170 | TailCallMap *ebpf.Map `ebpf:"tail_call_map"` 171 | } 172 | 173 | func (m *bpfMaps) Close() error { 174 | return _BpfClose( 175 | m.DummyEventMap, 176 | m.DummyRecordMap, 177 | m.GoProcs, 178 | m.MemBuckets, 179 | m.ProfilePid, 180 | m.ProfileState, 181 | m.SignalEvents, 182 | m.TailCallMap, 183 | ) 184 | } 185 | 186 | // bpfVariables contains all global variables after they have been loaded into the kernel. 187 | // 188 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 189 | type bpfVariables struct { 190 | } 191 | 192 | // bpfPrograms contains all programs after they have been loaded into the kernel. 193 | // 194 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 195 | type bpfPrograms struct { 196 | OomMarkVictimHandler *ebpf.Program `ebpf:"oom_mark_victim_handler"` 197 | RecordProfileBucketsProg *ebpf.Program `ebpf:"record_profile_buckets_prog"` 198 | SignalProbe *ebpf.Program `ebpf:"signal_probe"` 199 | } 200 | 201 | func (p *bpfPrograms) Close() error { 202 | return _BpfClose( 203 | p.OomMarkVictimHandler, 204 | p.RecordProfileBucketsProg, 205 | p.SignalProbe, 206 | ) 207 | } 208 | 209 | func _BpfClose(closers ...io.Closer) error { 210 | for _, closer := range closers { 211 | if err := closer.Close(); err != nil { 212 | return err 213 | } 214 | } 215 | return nil 216 | } 217 | 218 | // Do not access this directly. 219 | // 220 | //go:embed bpf_x86_bpfel.o 221 | var _BpfBytes []byte 222 | -------------------------------------------------------------------------------- /oomprof/bpf_x86_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parca-dev/oomprof/ec00408377fba70a12cada4e0c2fca5b04314ce7/oomprof/bpf_x86_bpfel.o -------------------------------------------------------------------------------- /oomprof/elfreader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package oomprof 15 | 16 | import ( 17 | "debug/buildinfo" 18 | "debug/elf" 19 | "encoding/binary" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "os" 24 | ) 25 | 26 | // SymbolInfo represents information about a symbol in an ELF file. 27 | type SymbolInfo struct { 28 | Name string 29 | Address uint64 30 | } 31 | 32 | // ELFReader is the interface for reading ELF files. 33 | // This allows users to provide their own optimized implementation 34 | // (e.g., using ebpf-profiler's pfelf) while oomprof provides a 35 | // default implementation using debug/elf. 36 | type ELFReader interface { 37 | // Open opens an ELF file for reading. 38 | Open(path string) (ELFFile, error) 39 | } 40 | 41 | // ELFFile represents an open ELF file. 42 | type ELFFile interface { 43 | // Close closes the ELF file. 44 | Close() error 45 | 46 | // GetBuildID returns the build ID of the ELF file. 47 | GetBuildID() (string, error) 48 | 49 | // GoVersion returns the Go version the binary was built with. 50 | // Returns empty string if not a Go binary or version cannot be determined. 51 | GoVersion() (string, error) 52 | 53 | // LookupSymbol looks up a symbol by name and returns its address. 54 | // Returns an error if the symbol is not found. 55 | LookupSymbol(name string) (SymbolInfo, error) 56 | } 57 | 58 | // defaultELFReader is the default implementation using debug/elf. 59 | type defaultELFReader struct{} 60 | 61 | // DefaultELFReader returns the default ELF reader implementation using debug/elf. 62 | func DefaultELFReader() ELFReader { 63 | return &defaultELFReader{} 64 | } 65 | 66 | func (r *defaultELFReader) Open(path string) (ELFFile, error) { 67 | f, err := os.Open(path) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | elfFile, err := elf.NewFile(f) 73 | if err != nil { 74 | f.Close() 75 | return nil, err 76 | } 77 | 78 | return &defaultELFFile{ 79 | file: f, 80 | elfFile: elfFile, 81 | }, nil 82 | } 83 | 84 | // defaultELFFile implements ELFFile using debug/elf. 85 | type defaultELFFile struct { 86 | file *os.File 87 | elfFile *elf.File 88 | } 89 | 90 | func (f *defaultELFFile) Close() error { 91 | f.elfFile.Close() 92 | return f.file.Close() 93 | } 94 | 95 | func (f *defaultELFFile) GetBuildID() (string, error) { 96 | // Try to find build ID in notes sections 97 | for _, prog := range f.elfFile.Progs { 98 | if prog.Type != elf.PT_NOTE { 99 | continue 100 | } 101 | 102 | notes, err := readNotes(prog.Open(), f.elfFile.ByteOrder) 103 | if err != nil { 104 | continue 105 | } 106 | 107 | for _, note := range notes { 108 | if note.Type == 3 && note.Name == "GNU" { // NT_GNU_BUILD_ID = 3 109 | return fmt.Sprintf("%x", note.Desc), nil 110 | } 111 | } 112 | } 113 | 114 | // Try .note.go.buildid section for Go binaries 115 | if sec := f.elfFile.Section(".note.go.buildid"); sec != nil { 116 | data, err := sec.Data() 117 | if err == nil && len(data) > 16 { 118 | // Skip the note header (16 bytes) and read the build ID 119 | return string(data[16:]), nil 120 | } 121 | } 122 | 123 | return "", errors.New("build ID not found") 124 | } 125 | 126 | func (f *defaultELFFile) GoVersion() (string, error) { 127 | bi, err := buildinfo.Read(f.file) 128 | if err != nil { 129 | return "", err 130 | } 131 | 132 | if bi != nil && bi.GoVersion != "" { 133 | return bi.GoVersion, nil 134 | } 135 | 136 | return "", errors.New("go version not found") 137 | } 138 | 139 | func (f *defaultELFFile) LookupSymbol(name string) (SymbolInfo, error) { 140 | symbols, err := f.elfFile.Symbols() 141 | if err != nil { 142 | // Try dynamic symbols if regular symbols fail 143 | symbols, err = f.elfFile.DynamicSymbols() 144 | if err != nil { 145 | return SymbolInfo{}, fmt.Errorf("failed to read symbols: %w", err) 146 | } 147 | } 148 | 149 | for _, sym := range symbols { 150 | if sym.Name == name { 151 | return SymbolInfo{ 152 | Name: sym.Name, 153 | Address: sym.Value, 154 | }, nil 155 | } 156 | } 157 | 158 | return SymbolInfo{}, fmt.Errorf("symbol %s not found", name) 159 | } 160 | 161 | // note represents an ELF note. 162 | type note struct { 163 | Name string 164 | Type uint32 165 | Desc []byte 166 | } 167 | 168 | // readNotes reads ELF notes from a reader. 169 | func readNotes(r io.Reader, order binary.ByteOrder) ([]note, error) { 170 | var notes []note 171 | 172 | for { 173 | var nameSize, descSize, noteType uint32 174 | 175 | // Read note header 176 | if err := binary.Read(r, order, &nameSize); err != nil { 177 | if err == io.EOF { 178 | break 179 | } 180 | return notes, err 181 | } 182 | if err := binary.Read(r, order, &descSize); err != nil { 183 | return notes, err 184 | } 185 | if err := binary.Read(r, order, ¬eType); err != nil { 186 | return notes, err 187 | } 188 | 189 | // Read name (padded to 4 bytes) 190 | namePadded := (nameSize + 3) &^ 3 191 | nameBytes := make([]byte, namePadded) 192 | if _, err := io.ReadFull(r, nameBytes); err != nil { 193 | return notes, err 194 | } 195 | 196 | name := string(nameBytes[:nameSize]) 197 | if nameSize > 0 && name[nameSize-1] == 0 { 198 | name = name[:nameSize-1] 199 | } 200 | 201 | // Read descriptor (padded to 4 bytes) 202 | descPadded := (descSize + 3) &^ 3 203 | desc := make([]byte, descPadded) 204 | if _, err := io.ReadFull(r, desc); err != nil { 205 | return notes, err 206 | } 207 | 208 | notes = append(notes, note{ 209 | Name: name, 210 | Type: noteType, 211 | Desc: desc[:descSize], 212 | }) 213 | } 214 | 215 | return notes, nil 216 | } 217 | 218 | // globalELFReader holds the current ELF reader implementation. 219 | var globalELFReader ELFReader = DefaultELFReader() 220 | 221 | // SetELFReader sets the global ELF reader implementation. 222 | // This allows users to provide their own optimized implementation. 223 | func SetELFReader(reader ELFReader) { 224 | globalELFReader = reader 225 | } 226 | 227 | // GetELFReader returns the current ELF reader implementation. 228 | func GetELFReader() ELFReader { 229 | return globalELFReader 230 | } 231 | -------------------------------------------------------------------------------- /oomprof/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | 12 | package oomprof 13 | 14 | import ( 15 | "os" 16 | 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // Set this to true when debug env var is set. 21 | var development bool 22 | 23 | func init() { 24 | _, dbgEnv := os.LookupEnv("OOM_DEBUG") 25 | development = dbgEnv 26 | } 27 | 28 | // logf logs debugging as higher level so they stick out w/o 29 | // enabling debug firehose if LUA_DEBUG env var is set. 30 | func logf(format string, args ...interface{}) { 31 | if development { 32 | logrus.Infof(format, args...) 33 | } else { 34 | logrus.Debugf(format, args...) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /oomprof/monitor_stub.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build !linux 15 | 16 | package oomprof 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | 22 | "github.com/google/pprof/profile" 23 | ) 24 | 25 | var ErrSelfWatch = errors.New("oomprof: cannot watch current process") 26 | var ErrNotLinux = errors.New("oomprof requires Linux kernel with eBPF support") 27 | 28 | func Setup() error { 29 | return ErrNotLinux 30 | } 31 | 32 | func Monitor(ctx context.Context) error { 33 | return ErrNotLinux 34 | } 35 | 36 | func GetState() (*State, error) { 37 | return nil, ErrNotLinux 38 | } 39 | 40 | func GetCurrentState() *State { 41 | return nil 42 | } 43 | 44 | func GetProfileForVictim(pid int) *profile.Profile { 45 | return nil 46 | } 47 | 48 | func GetProfile(pid int) *profile.Profile { 49 | return nil 50 | } 51 | 52 | func GetOOMChannel() <-chan int { 53 | return nil 54 | } 55 | 56 | func Shutdown() error { 57 | return ErrNotLinux 58 | } 59 | 60 | type State struct{} 61 | 62 | func (s *State) Stop() error { 63 | return ErrNotLinux 64 | } 65 | 66 | func (s *State) GetProfile(pid int) *profile.Profile { 67 | return nil 68 | } 69 | 70 | func (s *State) GetProfileForVictim(pid int) *profile.Profile { 71 | return nil 72 | } 73 | 74 | func (s *State) GetOOMChannel() <-chan int { 75 | return nil 76 | } 77 | 78 | func (s *State) GetDebugInfo() string { 79 | return "oomprof not available on non-Linux platforms" 80 | } 81 | 82 | func (s *State) Start() error { 83 | return ErrNotLinux 84 | } 85 | 86 | func (s *State) WatchPid(pid uint32) error { 87 | return ErrNotLinux 88 | } 89 | 90 | func (s *State) UnwatchPid(pid uint32) { 91 | } 92 | -------------------------------------------------------------------------------- /oomprof/oom_channel_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package oomprof_test 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "runtime" 9 | "testing" 10 | "time" 11 | 12 | "github.com/parca-dev/oomprof/oomprof" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func init() { 18 | // Set memory profile rate to 1 for maximum profile buckets 19 | runtime.MemProfileRate = 1 20 | } 21 | 22 | // TestOOMProfileChannelMode tests OOM profiling using the channel-based Setup call 23 | func TestOOMProfileChannelMode(t *testing.T) { 24 | // Skip if not root (required for eBPF) 25 | if os.Getuid() != 0 { 26 | t.Skip("Test requires root privileges") 27 | } 28 | 29 | // Create context with timeout 30 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 31 | defer cancel() 32 | 33 | // Create channel for profile data 34 | profileChan := make(chan oomprof.ProfileData, 10) 35 | 36 | // Configure oomprof 37 | config := &oomprof.Config{ 38 | ScanInterval: 0, // Disable process scanning 39 | MemLimit: 32 * 1024 * 1024, // 32MB 40 | LogTracePipe: false, 41 | Verbose: true, 42 | Symbolize: false, 43 | } 44 | 45 | // Start OOM profiler with channel mode 46 | state, err := oomprof.Setup(ctx, config, profileChan) 47 | require.NoError(t, err, "Failed to start oomprof") 48 | defer state.Close() 49 | 50 | // Track received profiles 51 | profileReceived := make(chan oomprof.ProfileData, 2) 52 | go func() { 53 | for { 54 | select { 55 | case <-ctx.Done(): 56 | return 57 | case profileData := <-profileChan: 58 | log.Infof("Received profile for PID %d, command %s", profileData.PID, profileData.Command) 59 | profileReceived <- profileData 60 | } 61 | } 62 | }() 63 | 64 | // Wait a bit for oomprof to set up monitoring 65 | time.Sleep(100 * time.Millisecond) 66 | 67 | // Get our PID 68 | selfPID := uint32(os.Getpid()) 69 | 70 | // Trigger profile collection 71 | err = state.ProfilePid(ctx, selfPID) 72 | if err != nil { 73 | // If symbolization fails, that's expected for test binaries 74 | t.Logf("ProfilePid returned error (may be expected for test binary): %v", err) 75 | // Continue the test as the profile may still be generated 76 | } 77 | 78 | // Wait for profile to be received (with longer timeout due to eBPF setup) 79 | select { 80 | case profileData := <-profileReceived: 81 | t.Log("Profile successfully received") 82 | require.Equal(t, selfPID, profileData.PID, "Profile should be for our PID") 83 | require.NotNil(t, profileData.Profile, "Profile should not be nil") 84 | require.True(t, len(profileData.Profile.Sample) > 0 || !profileData.Complete) 85 | if len(profileData.Profile.Sample) == 0 { 86 | t.Log("Warning: Profile has no samples (may be due to symbolization issues)") 87 | } else { 88 | t.Logf("Profile has %d samples", len(profileData.Profile.Sample)) 89 | } 90 | case <-time.After(10 * time.Second): 91 | t.Log("No profile received - this may be expected if eBPF profiling encounters issues") 92 | t.Log("The test validates that oomprof can be set up and ProfilePid can be called") 93 | return // Don't fail the test, just log 94 | } 95 | 96 | // Allocate some memory to make the profile more interesting 97 | allocateMemory() 98 | 99 | // Trigger another profile collection 100 | err = state.ProfilePid(ctx, selfPID) 101 | if err != nil { 102 | t.Logf("Second ProfilePid returned error (may be expected): %v", err) 103 | } 104 | 105 | // Wait for second profile 106 | select { 107 | case profileData := <-profileReceived: 108 | t.Log("Second profile successfully received") 109 | require.Equal(t, selfPID, profileData.PID, "Second profile should be for our PID") 110 | require.NotNil(t, profileData.Profile, "Second profile should not be nil") 111 | require.True(t, len(profileData.Profile.Sample) > 0 || !profileData.Complete) 112 | if len(profileData.Profile.Sample) == 0 { 113 | t.Log("Warning: Second profile has no samples") 114 | } else { 115 | t.Logf("Second profile has %d samples", len(profileData.Profile.Sample)) 116 | } 117 | require.Equal(t, len(profileData.Profile.SampleType), 2) 118 | case <-time.After(10 * time.Second): 119 | t.Log("Second profile not received - completing test") 120 | return // Complete the test successfully 121 | } 122 | } 123 | 124 | // allocateMemory allocates some memory to make the profile more interesting 125 | func allocateMemory() { 126 | // Allocate some memory chunks 127 | var chunks [][]byte 128 | for i := 0; i < 50; i++ { 129 | chunk := make([]byte, 1024*1024) // 1MB chunks 130 | // Fill with data to prevent optimization 131 | for j := range chunk { 132 | chunk[j] = byte(i + j) 133 | } 134 | chunks = append(chunks, chunk) 135 | } 136 | 137 | // Keep reference to prevent immediate GC 138 | runtime.KeepAlive(chunks) 139 | 140 | // Force GC to stabilize memory state 141 | runtime.GC() 142 | } 143 | -------------------------------------------------------------------------------- /oomprof/oomprof_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | // Copyright 2022-2025 The Parca Authors 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package oomprof 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "sort" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "github.com/containerd/cgroups/v3/cgroup2" 29 | log "github.com/sirupsen/logrus" 30 | "github.com/stretchr/testify/require" 31 | ) 32 | 33 | // setupTestCgroup creates a cgroup with specified memory limit for safe testing 34 | // Returns cleanup function and whether cgroup was successfully created 35 | func setupTestCgroup(t *testing.T, memLimitMB int64) (func(), bool) { 36 | // Create cgroup manager for oomprof-test 37 | memLimit := memLimitMB * 1024 * 1024 // Convert MB to bytes 38 | cgroupName := fmt.Sprintf("/oomprof-test-%dmb", memLimitMB) 39 | manager, err := cgroup2.NewManager("/sys/fs/cgroup", cgroupName, &cgroup2.Resources{ 40 | Memory: &cgroup2.Memory{ 41 | Max: &memLimit, 42 | }, 43 | }) 44 | if err != nil { 45 | t.Logf("WARNING: Failed to create cgroup (tests will run without memory limits): %v", err) 46 | return func() {}, false 47 | } 48 | 49 | // Cleanup function 50 | cleanup := func() { 51 | if err := manager.Delete(); err != nil { 52 | t.Logf("Failed to delete cgroup: %v", err) 53 | } 54 | } 55 | 56 | return cleanup, true 57 | } 58 | 59 | // runCommandInCgroup runs a command within the specified cgroup using a helper script 60 | func runCommandInCgroup(t *testing.T, cgroupPath string, originalCmd *exec.Cmd) *exec.Cmd { 61 | // Create a new command that uses the run-in-cgroup.sh script 62 | args := []string{cgroupPath, originalCmd.Path} 63 | args = append(args, originalCmd.Args[1:]...) 64 | 65 | cwd, err := os.Getwd() 66 | require.NoError(t, err) 67 | 68 | scriptPath := fmt.Sprintf("%s/run-in-cgroup.sh", cwd) 69 | 70 | cmd := exec.Command(scriptPath, args...) 71 | cmd.Dir = originalCmd.Dir 72 | cmd.Env = originalCmd.Env 73 | cmd.Stdin = originalCmd.Stdin 74 | cmd.Stdout = originalCmd.Stdout 75 | cmd.Stderr = originalCmd.Stderr 76 | 77 | return cmd 78 | } 79 | 80 | var testCfg = Config{ 81 | ScanInterval: 10 * time.Millisecond, 82 | MemLimit: 32, // Default memory limit in MB if not using cgroup 83 | Verbose: true, 84 | LogTracePipe: true, 85 | Symbolize: true, 86 | } 87 | 88 | func TestOOMProf(t *testing.T) { 89 | log.SetLevel(log.DebugLevel) 90 | // Check if go is available to decide which test to run 91 | _, goAvailable := exec.LookPath("go") 92 | 93 | if goAvailable == nil { 94 | // Full test with cgroups and multiple memory limits 95 | t.Log("Running full test suite with cgroups") 96 | memLimits := []int64{300, 1024, 2048} 97 | for _, memLimit := range memLimits { 98 | t.Run(fmt.Sprintf("%dMB", memLimit), func(t *testing.T) { 99 | testOOMProfWithMemLimit(t, memLimit) 100 | }) 101 | } 102 | } else { 103 | // Simplified test for QEMU environment 104 | t.Log("Running simplified QEMU test (go not available)") 105 | testOOMProfQEMUSimple(t) 106 | } 107 | } 108 | 109 | func testOOMProfWithMemLimit(t *testing.T, memLimitMB int64) { 110 | // Skip if not root 111 | if os.Getuid() != 0 { 112 | t.Skip("Test requires root privileges") 113 | } 114 | 115 | // Setup cgroup for safe testing 116 | cleanup, cgroupOK := setupTestCgroup(t, memLimitMB) 117 | defer cleanup() 118 | 119 | t.Logf("Testing with memory limit: %d MB", memLimitMB) 120 | 121 | // Create a context with cancel for clean shutdown 122 | ctx, cancel := context.WithCancel(context.Background()) 123 | defer cancel() 124 | 125 | // Create channel for profile data 126 | profileChan := make(chan ProfileData, 10) 127 | 128 | // Start OOM profiler 129 | state, err := Setup(ctx, &testCfg, profileChan) 130 | require.NoError(t, err) 131 | 132 | defer state.Close() 133 | 134 | profRateAll := []string{"GODEBUG=memprofilerate=1", "HOME=/root"} 135 | 136 | // Define test cases 137 | testCases := []struct { 138 | name string 139 | cmd string 140 | args []string 141 | expectedFunc string 142 | env []string 143 | cd string 144 | }{ 145 | // TODO: this test doensn't work because it oom's really quickly and the ebpf-profiler 146 | // may have not processed it yet. Not sure there's a solution, maybe a go program that 147 | // sleeps and then invokes the compiler? 148 | // { 149 | // name: "compiler", 150 | // cmd: "go", 151 | // args: []string{"build", "."}, 152 | // expectedFunc: "inline.TryInlineCall", 153 | // env: append(profRateAll, "GOTOOLCHAIN=go1.24.0"), 154 | // cd: "compile-oom", 155 | // }, 156 | { 157 | name: "big alloc", 158 | cmd: "./oomer.taux", 159 | args: []string{}, 160 | expectedFunc: "bigAlloc", 161 | env: profRateAll, 162 | }, 163 | { 164 | name: "small alloc", 165 | cmd: "./oomer.taux", 166 | args: []string{"--many"}, 167 | expectedFunc: "allocSpaceRecursive", 168 | env: profRateAll, 169 | }, 170 | { 171 | name: "gccache", 172 | cmd: "./gccache.taux", 173 | args: []string{"-entry-size", "8192", "-add-rate", "2000", "-expire-rate", "100"}, 174 | expectedFunc: "LeakyCache).Add", // Expect the Add method to show up prominently 175 | env: profRateAll, 176 | }, 177 | } 178 | 179 | // Check if required tools are available 180 | _, goAvailable := exec.LookPath("go") 181 | _, addr2lineAvailable := exec.LookPath("addr2line") 182 | 183 | maxRetries := 3 184 | 185 | for _, tc := range testCases { 186 | attempts := 0 187 | passed := false 188 | for !passed { 189 | if attempts >= maxRetries { 190 | t.Fatalf("Test %s failed after %d attempts", tc.name, attempts) 191 | } 192 | passed = t.Run(tc.name, func(t *testing.T) { 193 | attempts++ 194 | // Skip go build test if go is not available 195 | if tc.cmd == "go" && goAvailable != nil { 196 | t.Skipf("Skipping go build test: go not available (%v)", goAvailable) 197 | } 198 | 199 | // Start the OOM-triggering process 200 | cmd := exec.Command(tc.cmd, tc.args...) 201 | cmd.Env = tc.env 202 | cmd.Stderr = os.Stderr 203 | cmd.Stdout = os.Stdout 204 | cmd.Dir = tc.cd 205 | 206 | var finalCmd *exec.Cmd 207 | if cgroupOK { 208 | // Create command that runs in cgroup 209 | cgroupPath := fmt.Sprintf("/oomprof-test-%dmb", memLimitMB) 210 | finalCmd = runCommandInCgroup(t, cgroupPath, cmd) 211 | } else { 212 | // Run command directly without cgroup 213 | finalCmd = cmd 214 | } 215 | 216 | // Start the command in background 217 | err := finalCmd.Start() 218 | if err != nil { 219 | t.Fatalf("Failed to start %s: %v", tc.name, err) 220 | } 221 | 222 | // Wait for profile or timeout 223 | select { 224 | case profile := <-profileChan: 225 | t.Logf("Received profile for PID %d, command: %s", profile.PID, profile.Command) 226 | 227 | if profile.ReadError { 228 | return 229 | } 230 | 231 | // Validate profile has samples 232 | require.NotNil(t, profile.Profile) 233 | require.NotEmpty(t, profile.Profile.Sample, "Profile should have samples") 234 | 235 | // Calculate total memory allocated across all samples 236 | totalBytes := int64(0) 237 | totalAllocs := int64(0) 238 | for _, sample := range profile.Profile.Sample { 239 | if len(sample.Value) >= 2 { 240 | totalAllocs += sample.Value[0] 241 | totalBytes += sample.Value[1] 242 | } 243 | } 244 | t.Logf("Total allocations captured: %d allocs, %d bytes (%.2f MB)", totalAllocs, totalBytes, float64(totalBytes)/(1024*1024)) 245 | memLimitBytes := float64(memLimitMB * 1024 * 1024) 246 | t.Logf("Memory limit was %d MB, captured %.2f%% of limit", memLimitMB, float64(totalBytes)/memLimitBytes*100) 247 | 248 | // Check memprofilerate setting 249 | t.Logf("GODEBUG env: %v", tc.env) 250 | 251 | // Log top 5 samples by size 252 | type sampleInfo struct { 253 | idx int 254 | bytes int64 255 | } 256 | var topSamples []sampleInfo 257 | for i, sample := range profile.Profile.Sample { 258 | if len(sample.Value) >= 2 { 259 | topSamples = append(topSamples, sampleInfo{i, sample.Value[1]}) 260 | } 261 | } 262 | sort.Slice(topSamples, func(i, j int) bool { 263 | return topSamples[i].bytes > topSamples[j].bytes 264 | }) 265 | t.Logf("Top 5 samples by allocation size:") 266 | for i := 0; i < 5 && i < len(topSamples); i++ { 267 | t.Logf(" Sample %d: %d bytes (%.2f MB)", 268 | topSamples[i].idx, 269 | topSamples[i].bytes, 270 | float64(topSamples[i].bytes)/(1024*1024)) 271 | } 272 | 273 | // Find the heaviest sample and log its details 274 | maxBytes := int64(0) 275 | heaviestIdx := -1 276 | for i, sample := range profile.Profile.Sample { 277 | if len(sample.Value) >= 2 && sample.Value[1] > maxBytes { 278 | maxBytes = sample.Value[1] 279 | heaviestIdx = i 280 | } 281 | } 282 | if heaviestIdx >= 0 { 283 | t.Logf("Heaviest sample: index %d with %d bytes (%.2f MB)", heaviestIdx, maxBytes, float64(maxBytes)/(1024*1024)) 284 | // Log functions in heaviest sample 285 | t.Logf("Functions in heaviest sample:") 286 | for _, loc := range profile.Profile.Sample[heaviestIdx].Location { 287 | for _, line := range loc.Line { 288 | if line.Function != nil { 289 | t.Logf(" - %s", line.Function.Name) 290 | } 291 | } 292 | } 293 | } 294 | 295 | // First check if expected function is in the heaviest sample 296 | found := false 297 | if heaviestIdx >= 0 { 298 | t.Logf("Checking heaviest sample for function %s", tc.expectedFunc) 299 | for _, loc := range profile.Profile.Sample[heaviestIdx].Location { 300 | for _, line := range loc.Line { 301 | if line.Function != nil && strings.Contains(line.Function.Name, tc.expectedFunc) { 302 | found = true 303 | t.Logf("✓ Found expected function %s in heaviest sample (index %d)", tc.expectedFunc, heaviestIdx) 304 | break 305 | } 306 | } 307 | if found { 308 | break 309 | } 310 | } 311 | } 312 | 313 | // If not in heaviest sample, log where it actually is 314 | if !found { 315 | t.Logf("Function %s NOT in heaviest sample, searching all samples...", tc.expectedFunc) 316 | // First, log all functions containing "inline" to debug 317 | if tc.name == "go build with TryInlineCall" { 318 | t.Logf("All functions containing 'inline':") 319 | for i, sample := range profile.Profile.Sample { 320 | for _, loc := range sample.Location { 321 | for _, line := range loc.Line { 322 | if line.Function != nil && strings.Contains(line.Function.Name, "inline") { 323 | allocBytes := int64(0) 324 | if len(sample.Value) >= 2 { 325 | allocBytes = sample.Value[1] 326 | } 327 | t.Logf(" Sample %d: %s (%d bytes)", i, line.Function.Name, allocBytes) 328 | } 329 | } 330 | } 331 | } 332 | } 333 | 334 | for i, sample := range profile.Profile.Sample { 335 | for _, loc := range sample.Location { 336 | for _, line := range loc.Line { 337 | if line.Function != nil && strings.Contains(line.Function.Name, tc.expectedFunc) { 338 | found = true 339 | allocBytes := int64(0) 340 | if len(sample.Value) >= 2 { 341 | allocBytes = sample.Value[1] 342 | } 343 | t.Logf("Found expected function %s in sample %d (bytes=%d, %.2f MB)", tc.expectedFunc, i, allocBytes, float64(allocBytes)/(1024*1024)) 344 | goto done 345 | } 346 | } 347 | } 348 | } 349 | } 350 | done: 351 | 352 | // Skip function name checking if addr2line is not available 353 | if addr2lineAvailable != nil { 354 | t.Logf("Skipping function name validation: addr2line not available (%v)", addr2lineAvailable) 355 | } else { 356 | // For go build test, we expect the function to be in the heaviest sample 357 | // But only if we captured a reasonable amount of memory 358 | if tc.name == "go build with TryInlineCall" && heaviestIdx >= 0 { 359 | // If we captured less than 1% of memory limit, the profile might be incomplete 360 | captureRatio := float64(totalBytes) / memLimitBytes 361 | if captureRatio < 0.01 { 362 | t.Logf("WARNING: Only captured %.2f%% of memory limit, profile may be incomplete", captureRatio*100) 363 | t.Logf("This suggests the process was using memory outside the Go heap (mmap, stack, etc)") 364 | } 365 | 366 | foundInHeaviest := false 367 | for _, loc := range profile.Profile.Sample[heaviestIdx].Location { 368 | for _, line := range loc.Line { 369 | if line.Function != nil && strings.Contains(line.Function.Name, tc.expectedFunc) { 370 | foundInHeaviest = true 371 | break 372 | } 373 | } 374 | if foundInHeaviest { 375 | break 376 | } 377 | } 378 | // Since we're capturing so little memory, let's just check if the function exists anywhere 379 | if captureRatio < 0.01 { 380 | require.True(t, found, "Expected function %s not found in profile", tc.expectedFunc) 381 | } else { 382 | require.True(t, foundInHeaviest, "Expected function %s to be in heaviest sample, but was not", tc.expectedFunc) 383 | } 384 | } else { 385 | require.True(t, found, "Expected function %s not found in profile", tc.expectedFunc) 386 | } 387 | } 388 | 389 | case <-time.After(45 * time.Second): 390 | t.Logf("WARNING: Timeout waiting for profile in test %s (process may have been OOM killed without profile capture)", tc.name) 391 | } 392 | 393 | // Make sure oom finishes 394 | if err := finalCmd.Wait(); err != nil { 395 | t.Logf("Process %s exited with err: %v", tc.name, err) 396 | } 397 | }) 398 | } 399 | } 400 | 401 | // Cancel context to clean up 402 | cancel() 403 | } 404 | 405 | func testOOMProfQEMUSimple(t *testing.T) { 406 | // Skip if not root 407 | if os.Getuid() != 0 { 408 | t.Skip("Test requires root privileges") 409 | } 410 | 411 | t.Logf("Testing QEMU environment with simplified tests") 412 | 413 | // Create a context with cancel for clean shutdown 414 | ctx, cancel := context.WithCancel(context.Background()) 415 | defer cancel() 416 | 417 | // Create channel for profile data 418 | profileChan := make(chan ProfileData, 10) 419 | 420 | cfg := Config{ 421 | LogTracePipe: true, 422 | Verbose: true, 423 | } 424 | // Start OOM profiler 425 | state, err := Setup(ctx, &cfg, profileChan) 426 | require.NoError(t, err) 427 | defer state.Close() 428 | 429 | profRateAll := []string{"GODEBUG=memprofilerate=1", "HOME=/root"} 430 | 431 | // Define simplified test cases for QEMU - only oomer and gccache, skip symbolic validation 432 | testCases := []struct { 433 | name string 434 | cmd string 435 | args []string 436 | env []string 437 | checkPid bool 438 | }{ 439 | { 440 | name: "oomer big alloc", 441 | cmd: "./oomer.taux", 442 | args: []string{}, 443 | env: profRateAll, 444 | checkPid: true, 445 | }, 446 | { 447 | name: "oomer small allocs", 448 | cmd: "./oomer.taux", 449 | args: []string{"--many"}, 450 | env: profRateAll, 451 | checkPid: true, 452 | }, 453 | { 454 | name: "gc cache with cycles", 455 | cmd: "./gccache.taux", 456 | args: []string{"-entry-size", "8192", "-add-rate", "2000", "-expire-rate", "100"}, 457 | env: profRateAll, 458 | checkPid: true, 459 | }, 460 | } 461 | 462 | for _, tc := range testCases { 463 | t.Run(tc.name, func(t *testing.T) { 464 | // Create command 465 | cmd := exec.Command(tc.cmd, tc.args...) 466 | cmd.Env = tc.env 467 | 468 | // Start command 469 | err := cmd.Start() 470 | require.NoError(t, err, "Failed to start command %s", tc.cmd) 471 | 472 | finalCmd := cmd 473 | 474 | // Wait for either profile data or timeout 475 | select { 476 | case profile := <-profileChan: 477 | t.Logf("Received profile for PID %d, command: %s", profile.PID, profile.Command) 478 | 479 | // Basic validation - just check we got some data 480 | require.NotNil(t, profile.Profile) 481 | require.NotEmpty(t, profile.Profile.Sample, "Profile should have samples") 482 | 483 | // Calculate total memory allocated across all samples 484 | totalBytes := int64(0) 485 | totalAllocs := int64(0) 486 | for _, sample := range profile.Profile.Sample { 487 | if len(sample.Value) >= 2 { 488 | totalAllocs += sample.Value[0] 489 | totalBytes += sample.Value[1] 490 | } 491 | } 492 | 493 | t.Logf("Total allocations captured: %d allocs, %d bytes (%.2f MB)", 494 | totalAllocs, totalBytes, float64(totalBytes)/(1024*1024)) 495 | 496 | // Skip symbolic validation in QEMU environment 497 | t.Logf("Skipping symbolic validation in QEMU environment") 498 | 499 | case <-time.After(60 * time.Second): 500 | t.Logf("WARNING: Timeout waiting for profile in test %s, but process may have been killed", tc.name) 501 | // The process might have been OOM killed but we didn't capture the profile 502 | // This can happen if the eBPF program fails to capture the event 503 | } 504 | 505 | // Make sure process finishes 506 | if err := finalCmd.Wait(); err != nil { 507 | t.Logf("Process %s exited with err: %v", tc.name, err) 508 | } 509 | }) 510 | } 511 | 512 | // Cancel context to clean up 513 | cancel() 514 | } 515 | 516 | func TestSingletonSetup(t *testing.T) { 517 | // Skip if not root 518 | if os.Getuid() != 0 { 519 | t.Skip("Test requires root privileges") 520 | } 521 | 522 | t.Run("GetStateBeforeSetup", func(t *testing.T) { 523 | // Reset singleton state for clean test 524 | setupMutex.Lock() 525 | globalState = nil 526 | isInitialized = false 527 | setupMutex.Unlock() 528 | 529 | _, err := GetState() 530 | require.ErrorIs(t, err, ErrNotInitialized) 531 | }) 532 | 533 | t.Run("SingleSetup", func(t *testing.T) { 534 | // Reset singleton state for clean test 535 | setupMutex.Lock() 536 | globalState = nil 537 | isInitialized = false 538 | setupMutex.Unlock() 539 | 540 | ctx := context.Background() 541 | profileChan := make(chan ProfileData, 10) 542 | 543 | cfg := &Config{ 544 | ScanInterval: 100 * time.Millisecond, 545 | Verbose: false, 546 | LogTracePipe: false, 547 | Symbolize: false, 548 | } 549 | 550 | state, err := Setup(ctx, cfg, profileChan) 551 | require.NoError(t, err) 552 | require.NotNil(t, state) 553 | 554 | // Test GetState returns the same instance 555 | retrievedState, err := GetState() 556 | require.NoError(t, err) 557 | require.Equal(t, state, retrievedState) 558 | 559 | state.Close() 560 | 561 | // After cleanup, GetState should return error 562 | _, err = GetState() 563 | require.ErrorIs(t, err, ErrNotInitialized) 564 | }) 565 | 566 | t.Run("DoubleSetup", func(t *testing.T) { 567 | // Reset singleton state for clean test 568 | setupMutex.Lock() 569 | globalState = nil 570 | isInitialized = false 571 | setupMutex.Unlock() 572 | 573 | ctx := context.Background() 574 | profileChan1 := make(chan ProfileData, 10) 575 | profileChan2 := make(chan ProfileData, 10) 576 | 577 | cfg := &Config{ 578 | ScanInterval: 100 * time.Millisecond, 579 | Verbose: false, 580 | LogTracePipe: false, 581 | Symbolize: false, 582 | } 583 | 584 | // First setup should succeed 585 | state1, err := Setup(ctx, cfg, profileChan1) 586 | require.NoError(t, err) 587 | 588 | // Second setup should fail 589 | _, err = Setup(ctx, cfg, profileChan2) 590 | require.ErrorIs(t, err, ErrAlreadyInitialized) 591 | 592 | state1.Close() 593 | }) 594 | 595 | t.Run("SetupWithReporterDoubleSetup", func(t *testing.T) { 596 | // Reset singleton state for clean test 597 | setupMutex.Lock() 598 | globalState = nil 599 | isInitialized = false 600 | setupMutex.Unlock() 601 | 602 | ctx := context.Background() 603 | 604 | cfg := &Config{ 605 | ScanInterval: 100 * time.Millisecond, 606 | Verbose: false, 607 | LogTracePipe: false, 608 | Symbolize: false, 609 | } 610 | 611 | // First setup with reporter should succeed 612 | state1, err := SetupWithReporter(ctx, cfg, nil) // nil reporter for testing 613 | require.NoError(t, err) 614 | 615 | // Second setup (different type) should also fail 616 | profileChan := make(chan ProfileData, 10) 617 | _, err = Setup(ctx, cfg, profileChan) 618 | require.ErrorIs(t, err, ErrAlreadyInitialized) 619 | 620 | state1.Close() 621 | }) 622 | } 623 | 624 | func TestWatchPidSelfWatch(t *testing.T) { 625 | // Skip if not root 626 | if os.Getuid() != 0 { 627 | t.Skip("Test requires root privileges") 628 | } 629 | 630 | // Reset singleton state for clean test 631 | setupMutex.Lock() 632 | globalState = nil 633 | isInitialized = false 634 | setupMutex.Unlock() 635 | 636 | ctx := context.Background() 637 | profileChan := make(chan ProfileData, 10) 638 | 639 | cfg := &Config{ 640 | ScanInterval: 100 * time.Millisecond, 641 | Verbose: false, 642 | LogTracePipe: false, 643 | Symbolize: false, 644 | } 645 | 646 | state, err := Setup(ctx, cfg, profileChan) 647 | require.NoError(t, err) 648 | defer state.Close() 649 | 650 | // Try to watch our own PID - should return ErrSelfWatch 651 | currentPID := uint32(os.Getpid()) 652 | err = state.WatchPid(currentPID) 653 | require.ErrorIs(t, err, ErrSelfWatch) 654 | 655 | // Watching a different PID should work (even if it doesn't exist) 656 | differentPID := currentPID + 1 657 | err = state.WatchPid(differentPID) 658 | require.NoError(t, err) // Should not error even if PID doesn't exist 659 | } 660 | -------------------------------------------------------------------------------- /oomprof/pprof.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build linux 15 | 16 | package oomprof 17 | 18 | import ( 19 | "fmt" 20 | "os/exec" 21 | "strconv" 22 | "strings" 23 | "time" 24 | 25 | "github.com/google/pprof/profile" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | func bucketsToPprof(buckets []bpfGobucket, binaryPath string, buildID string, symbolize bool, reportAlloc bool) (*profile.Profile, error) { 30 | // Create a new pprof profile 31 | prof := &profile.Profile{ 32 | DefaultSampleType: "inuse_space", 33 | SampleType: []*profile.ValueType{ 34 | {Type: "inuse_objects", Unit: "count"}, 35 | {Type: "inuse_space", Unit: "bytes"}, 36 | }, 37 | PeriodType: &profile.ValueType{Type: "space", Unit: "bytes"}, 38 | Period: 512 * 1024, // TODO: read this from process 39 | TimeNanos: time.Now().UnixNano(), 40 | } 41 | 42 | if reportAlloc { 43 | prof.SampleType = append(prof.SampleType, 44 | &profile.ValueType{Type: "alloc_objects", Unit: "count"}, 45 | &profile.ValueType{Type: "alloc_space", Unit: "bytes"}) 46 | 47 | } 48 | 49 | // Create a mapping for the binary if we have the path 50 | var mainMapping *profile.Mapping 51 | if binaryPath != "" { 52 | mainMapping = &profile.Mapping{ 53 | ID: 1, 54 | Start: 0, 55 | Limit: ^uint64(0), // Use max uint64 as we don't know the actual size 56 | File: binaryPath, 57 | BuildID: buildID, 58 | } 59 | prof.Mapping = append(prof.Mapping, mainMapping) 60 | } 61 | 62 | // Track unique locations and functions 63 | locationMap := make(map[uint64]*profile.Location) 64 | functionMap := make(map[uint64]*profile.Function) 65 | nextLocationID := uint64(1) 66 | nextFunctionID := uint64(1) 67 | 68 | // Collect all unique addresses first 69 | uniqueAddrs := make(map[uint64]bool) 70 | for _, bucket := range buckets { 71 | mr := bucket.Mem 72 | allocs := mr.Active.Allocs 73 | inuse := mr.Active.Allocs - mr.Active.Frees 74 | for i := 0; i < 3; i++ { 75 | allocs += mr.Future[i].Allocs 76 | inuse += mr.Future[i].Allocs - mr.Future[i].Frees 77 | } 78 | // Skip buckets based on ReportAlloc setting 79 | if !reportAlloc && inuse == 0 { 80 | // When only reporting inuse, skip buckets with zero inuse 81 | continue 82 | } 83 | 84 | stackLen := int(bucket.Header.Nstk) 85 | for i := 0; i < stackLen; i++ { 86 | addr := bucket.Stk[i] 87 | if addr != 0 { 88 | uniqueAddrs[addr] = true 89 | } 90 | } 91 | } 92 | 93 | // Batch symbolize all addresses at once 94 | symbolMap := make(map[uint64]symbolInfo) 95 | if binaryPath != "" && len(uniqueAddrs) > 0 && symbolize { 96 | symbolMap = batchResolveSymbols(binaryPath, uniqueAddrs) 97 | } 98 | 99 | // Process each bucket 100 | for b, _ := range buckets { 101 | mr := buckets[b].Mem 102 | allocs, allocBytes := mr.Active.Allocs, mr.Active.AllocBytes 103 | inuse, inuseBytes := mr.Active.Allocs-mr.Active.Frees, mr.Active.AllocBytes-mr.Active.FreeBytes 104 | for i := 0; i < 3; i++ { 105 | allocs += mr.Future[i].Allocs 106 | allocBytes += mr.Future[i].AllocBytes 107 | inuse += mr.Future[i].Allocs - mr.Future[i].Frees 108 | inuseBytes += mr.Future[i].AllocBytes - mr.Future[i].FreeBytes 109 | } 110 | if !reportAlloc && inuse == 0 { 111 | // When only reporting inuse, skip buckets with zero inuse 112 | continue 113 | } 114 | 115 | // Create locations for the stack trace 116 | var locations []*profile.Location 117 | 118 | // Process stack frames (up to nstk) 119 | stackLen := int(buckets[b].Header.Nstk) 120 | 121 | for i := 0; i < stackLen; i++ { 122 | addr := buckets[b].Stk[i] 123 | if addr == 0 { 124 | break 125 | } 126 | 127 | // Check if we already have this location 128 | loc, exists := locationMap[addr] 129 | if !exists { 130 | // Create a new location 131 | loc = &profile.Location{ 132 | ID: nextLocationID, 133 | Address: addr, 134 | Mapping: mainMapping, 135 | } 136 | nextLocationID++ 137 | 138 | // Create a function for this location using symbol resolution 139 | _, fnExists := functionMap[addr] 140 | if !fnExists { 141 | var funcName, location string 142 | var lineNum int64 = 1 143 | 144 | if symInfo, ok := symbolMap[addr]; ok { 145 | funcName = symInfo.name 146 | location = symInfo.file 147 | lineNum = symInfo.line 148 | 149 | fn := &profile.Function{ 150 | ID: nextFunctionID, 151 | Name: funcName, 152 | SystemName: funcName, 153 | Filename: location, 154 | StartLine: lineNum, 155 | } 156 | nextFunctionID++ 157 | functionMap[addr] = fn 158 | prof.Function = append(prof.Function, fn) 159 | 160 | // Add function to location 161 | loc.Line = []profile.Line{ 162 | { 163 | Function: fn, 164 | Line: 1, 165 | }, 166 | } 167 | 168 | } 169 | } 170 | 171 | locationMap[addr] = loc 172 | prof.Location = append(prof.Location, loc) 173 | } 174 | 175 | locations = append(locations, loc) 176 | } 177 | 178 | // Create a sample 179 | values := []int64{int64(inuse), int64(inuseBytes)} 180 | if reportAlloc { 181 | values = append(values, int64(allocs), int64(allocBytes)) 182 | } 183 | sample := &profile.Sample{ 184 | Location: locations, 185 | Value: values, 186 | } 187 | prof.Sample = append(prof.Sample, sample) 188 | } 189 | 190 | // Sort locations by ID 191 | for i := range prof.Location { 192 | for j := i + 1; j < len(prof.Location); j++ { 193 | if prof.Location[i].ID > prof.Location[j].ID { 194 | prof.Location[i], prof.Location[j] = prof.Location[j], prof.Location[i] 195 | } 196 | } 197 | } 198 | 199 | return prof, nil 200 | } 201 | 202 | type symbolInfo struct { 203 | name string 204 | file string 205 | line int64 206 | } 207 | 208 | // batchResolveSymbols uses a single addr2line call to resolve all addresses at once 209 | func batchResolveSymbols(binaryPath string, addrs map[uint64]bool) map[uint64]symbolInfo { 210 | result := make(map[uint64]symbolInfo) 211 | 212 | if len(addrs) == 0 { 213 | return result 214 | } 215 | 216 | // Build address list 217 | var addrList []string 218 | var addrOrder []uint64 219 | for addr := range addrs { 220 | addrList = append(addrList, fmt.Sprintf("0x%x", addr)) 221 | addrOrder = append(addrOrder, addr) 222 | } 223 | 224 | log.WithField("count", len(addrList)).Debug("Batch symbolizing addresses") 225 | startTime := time.Now() 226 | 227 | // Call addr2line with all addresses at once 228 | cmd := exec.Command("addr2line", append([]string{"-e", binaryPath, "-f", "-C"}, addrList...)...) 229 | output, err := cmd.Output() 230 | if err != nil { 231 | log.WithError(err).Debug("addr2line batch call failed") 232 | // Return empty symbols 233 | for _, addr := range addrOrder { 234 | result[addr] = symbolInfo{ 235 | name: fmt.Sprintf("func_%x", addr), 236 | file: "", 237 | line: 0, 238 | } 239 | } 240 | return result 241 | } 242 | 243 | lines := strings.Split(strings.TrimSpace(string(output)), "\n") 244 | 245 | // addr2line outputs 2 lines per address: function name, then file:line 246 | for i := 0; i < len(addrOrder) && i*2+1 < len(lines); i++ { 247 | addr := addrOrder[i] 248 | funcName := strings.TrimSpace(lines[i*2]) 249 | location := strings.TrimSpace(lines[i*2+1]) 250 | 251 | var lineNum int64 = 1 252 | var fileName string = location 253 | 254 | // Extract line number if available 255 | if parts := strings.Split(location, ":"); len(parts) >= 2 { 256 | fileName = parts[0] 257 | if num, err := strconv.ParseInt(parts[1], 10, 64); err == nil { 258 | lineNum = num 259 | } 260 | } 261 | 262 | // Use address as function name if symbolization failed 263 | if funcName == "??" || location == "??:0" { 264 | funcName = fmt.Sprintf("func_%x", addr) 265 | fileName = "" 266 | lineNum = 0 267 | } 268 | 269 | result[addr] = symbolInfo{ 270 | name: funcName, 271 | file: fileName, 272 | line: lineNum, 273 | } 274 | } 275 | 276 | log.WithField("duration", time.Since(startTime)).Debug("Batch symbolization completed") 277 | return result 278 | } 279 | -------------------------------------------------------------------------------- /oomprof/reporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package oomprof 15 | 16 | // Reporter is the interface that oomprof clients must implement to receive oom memory profile events. 17 | type Reporter interface { 18 | // SampleEvents reports a batch of samples with their metadata. 19 | // This is called once for every 1000 buckets to reduce overhead. 20 | SampleEvents(samples []Sample, meta SampleMeta) error 21 | } 22 | 23 | // Address represents a memory address. 24 | type Address uint64 25 | 26 | // Sample represents a profiling sample with stack trace and metrics. 27 | type Sample struct { 28 | // Addresses contains the stack frame addresses. 29 | Addresses []Address 30 | // Allocs is the number of allocations. 31 | Allocs uint64 32 | // Frees is the number of frees. 33 | Frees uint64 34 | // AllocBytes is the number of bytes allocated. 35 | AllocBytes uint64 36 | // FreeBytes is the number of bytes freed. 37 | FreeBytes uint64 38 | } 39 | 40 | // SampleMeta contains metadata for a trace event. 41 | type SampleMeta struct { 42 | // Timestamp is the time the event occurred. 43 | Timestamp uint64 44 | // Comm is the command name of the process. 45 | Comm string 46 | // ProcessName is the name of the process. 47 | ProcessName string 48 | // ExecutablePath is the full path to the executable. 49 | ExecutablePath string 50 | // PID is the process ID. 51 | PID uint32 52 | // buildid for exe 53 | BuildID string 54 | // CustomLabels contains custom labels associated with the memory profile. 55 | CustomLabels map[string]string 56 | } 57 | -------------------------------------------------------------------------------- /oomprof/scan.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | //go:build linux 15 | 16 | package oomprof 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | "strconv" 23 | "strings" 24 | "sync" 25 | 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | // scanGoProcesses scans the /proc filesystem for running Go processes 30 | // and returns a slice of GoProcessInfo structs with PID and mbuckets address 31 | func scanGoProcesses(ctx context.Context, goProcs map[uint32]int64, pidToExeInfo *sync.Map) ([]GoProcessInfo, error) { 32 | var results []GoProcessInfo 33 | 34 | // Open /proc directory 35 | procDir, err := os.Open("/proc") 36 | if err != nil { 37 | return nil, fmt.Errorf("error opening /proc: %v", err) 38 | } 39 | defer procDir.Close() 40 | 41 | // Read all directory entries 42 | entries, err := procDir.Readdir(-1) 43 | if err != nil { 44 | return nil, fmt.Errorf("error reading /proc directory: %v", err) 45 | } 46 | 47 | // Iterate through all entries in /proc 48 | for _, entry := range entries { 49 | // Check for context cancellation 50 | select { 51 | case <-ctx.Done(): 52 | return results, ctx.Err() 53 | default: 54 | } 55 | // We're only interested in directories with numeric names (PIDs) 56 | if !entry.IsDir() { 57 | continue 58 | } 59 | 60 | pid, err := strconv.Atoi(entry.Name()) 61 | if err != nil { 62 | // Not a PID directory 63 | continue 64 | } 65 | 66 | if goProcs[uint32(pid)] != 0 { 67 | // Already in the map, skip 68 | continue 69 | } 70 | 71 | // Get the executable path for this PID 72 | exePath := fmt.Sprintf("/proc/%d/exe", pid) 73 | 74 | // Resolve the actual executable path by following the symlink 75 | realExePath, err := os.Readlink(exePath) 76 | if err != nil { 77 | // Skip if we can't read the exe link (usually permission issues) 78 | goProcs[uint32(pid)] = -1 79 | //log.Printf("error reading exe link for PID %d: %v", pid, err) 80 | continue 81 | } 82 | 83 | // Try to read the command line to get more info 84 | cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid) 85 | cmdline, err := os.ReadFile(cmdlinePath) 86 | if err != nil { 87 | // Skip if we can't read the command line (usually permission issues) 88 | goProcs[uint32(pid)] = -1 89 | log.WithError(err).WithField("pid", pid).Debug("error reading cmdline for PID") 90 | continue 91 | } 92 | commPath := fmt.Sprintf("/proc/%d/comm", pid) 93 | comm, err := os.ReadFile(commPath) 94 | if err != nil { 95 | // Skip if we can't read the command line (usually permission issues) 96 | goProcs[uint32(pid)] = -1 97 | log.WithError(err).WithField("pid", pid).Debug("error reading comm for PID") 98 | continue 99 | } 100 | 101 | cmdlineStr := strings.Replace(string(cmdline), "\x00", " ", -1) 102 | 103 | // Try to open the executable using the ELF reader 104 | elfReader := GetELFReader() 105 | elfFile, err := elfReader.Open(exePath) 106 | if err != nil { 107 | // Skip if we can't open the executable (usually permission issues) 108 | goProcs[uint32(pid)] = -1 109 | log.WithError(err).WithField("pid", pid).Debug("error opening ELF file for PID") 110 | continue 111 | } 112 | 113 | // Get Go version for this executable 114 | goVersion, err := elfFile.GoVersion() 115 | if err != nil || goVersion == "" { 116 | elfFile.Close() 117 | goProcs[uint32(pid)] = -1 118 | //log.Printf("error getting Go version for PID %d: %v", pid, err) 119 | continue 120 | } 121 | 122 | // Get the BuildID 123 | buildID, err := elfFile.GetBuildID() 124 | if err != nil { 125 | log.WithError(err).WithField("pid", pid).Debug("error getting build ID for PID") 126 | buildID = "" // Use empty string if we can't get the build ID 127 | } 128 | 129 | // This is a Go program - look up the mbuckets symbol 130 | mbucketsAddr, err := elfFile.LookupSymbol("runtime.mbuckets") 131 | if err != nil { 132 | log.WithError(err).Error("error looking up mbuckets symbol") 133 | goProcs[uint32(pid)] = -1 134 | continue 135 | } 136 | 137 | goProcs[uint32(pid)] = int64(mbucketsAddr.Address) 138 | log.WithFields(log.Fields{"pid": pid, "mbuckets": fmt.Sprintf("%x", mbucketsAddr.Address), "comm": strings.TrimSpace(string(comm)), "cmdline": cmdlineStr, "buildid": buildID}).Debug("oomprof: found Go program") 139 | 140 | // Store the PID to exe info mapping in the sync.Map 141 | exeInfo := &ExeInfo{ 142 | Path: realExePath, 143 | BuildID: buildID, 144 | } 145 | pidToExeInfo.Store(uint32(pid), exeInfo) 146 | 147 | // Create a GoProcessInfo struct and add it to our results 148 | procInfo := GoProcessInfo{ 149 | PID: uint32(pid), 150 | GoVersion: goVersion, 151 | MBucketsAddr: uint64(mbucketsAddr.Address), 152 | CmdLine: cmdlineStr, 153 | ExePath: realExePath, 154 | } 155 | 156 | results = append(results, procInfo) 157 | elfFile.Close() 158 | } 159 | 160 | return results, nil 161 | } 162 | 163 | // GoProcessInfo holds information about a running Go process. 164 | type GoProcessInfo struct { 165 | // PID is the process ID. 166 | PID uint32 167 | // GoVersion is the Go version of the process. 168 | GoVersion string 169 | // MBucketsAddr is the address of the mbuckets symbol in memory. 170 | MBucketsAddr uint64 171 | // CmdLine is the command line used to start the process. 172 | CmdLine string 173 | // ExePath is the path to the executable. 174 | ExePath string 175 | } 176 | -------------------------------------------------------------------------------- /oomprof/trace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package oomprof 15 | 16 | import ( 17 | "bufio" 18 | "context" 19 | "errors" 20 | "io" 21 | "log" 22 | "os" 23 | "strings" 24 | ) 25 | 26 | func getTracePipe() (*os.File, error) { 27 | for _, mnt := range []string{ 28 | "/sys/kernel/debug/tracing", 29 | "/sys/kernel/tracing", 30 | "/tracing", 31 | "/trace"} { 32 | t, err := os.Open(mnt + "/trace_pipe") 33 | if err == nil { 34 | return t, nil 35 | } 36 | log.Printf("Could not open trace_pipe at %s: %s", mnt, err) 37 | } 38 | return nil, os.ErrNotExist 39 | } 40 | 41 | func readTracePipe(ctx context.Context) { 42 | tp, err := getTracePipe() 43 | if err != nil { 44 | log.Printf("Could not open trace_pipe, check that debugfs is mounted") 45 | return 46 | } 47 | 48 | // When we're done kick ReadString out of blocked I/O. 49 | go func() { 50 | <-ctx.Done() 51 | tp.Close() 52 | }() 53 | 54 | r := bufio.NewReader(tp) 55 | for { 56 | line, err := r.ReadString('\n') 57 | if err != nil { 58 | if errors.Is(err, io.EOF) { 59 | continue 60 | } 61 | if errors.Is(err, os.ErrClosed) { 62 | return 63 | } 64 | log.Print(err) 65 | return 66 | } 67 | line = strings.TrimSpace(line) 68 | if line != "" { 69 | log.Printf("%s", line) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/compile-oom/go.mod: -------------------------------------------------------------------------------- 1 | module compile-oom-test 2 | 3 | go 1.24.0 4 | -------------------------------------------------------------------------------- /tests/compile-oom/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import "fmt" 17 | 18 | // https://github.com/golang/go/issues/72063 19 | // Y is the Y-combinator based on https://dreamsongs.com/Files/WhyOfY.pdf 20 | func Y[Endo ~func(RecFct) RecFct, RecFct ~func(T) R, T, R any](f Endo) RecFct { 21 | 22 | type internal[RecFct ~func(T) R, T, R any] func(internal[RecFct, T, R]) RecFct 23 | 24 | g := func(h internal[RecFct, T, R]) RecFct { 25 | return func(t T) R { 26 | return f(h(h))(t) 27 | } 28 | } 29 | return g(g) 30 | } 31 | 32 | func main() { 33 | 34 | fct := Y(func(r func(int) int) func(int) int { 35 | return func(n int) int { 36 | if n <= 0 { 37 | return 1 38 | } 39 | return n * r(n-1) 40 | } 41 | }) 42 | 43 | fmt.Println(fct(10)) 44 | } 45 | -------------------------------------------------------------------------------- /tests/deepstack/go.mod: -------------------------------------------------------------------------------- 1 | module deepstack 2 | 3 | go 1.21 -------------------------------------------------------------------------------- /tests/deepstack/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | "net/http" 20 | "net/http/pprof" 21 | "os" 22 | "runtime" 23 | "time" 24 | ) 25 | 26 | const allocSize = 4096 27 | 28 | // Global variable to prevent compiler optimization 29 | var keepAlive [][]byte 30 | 31 | // Track which functions have been called in the current call path to avoid recursion 32 | type callStack struct { 33 | called [8]bool 34 | } 35 | 36 | func (cs *callStack) canCall(funcNum int) bool { 37 | return !cs.called[funcNum] 38 | } 39 | 40 | func (cs *callStack) markCalled(funcNum int) callStack { 41 | newCS := *cs 42 | newCS.called[funcNum] = true 43 | return newCS 44 | } 45 | 46 | func func0(cs callStack) { 47 | data := make([]byte, allocSize) 48 | keepAlive = append(keepAlive, data) 49 | 50 | newCS := cs.markCalled(0) 51 | 52 | // Call all other functions that haven't been called yet 53 | if newCS.canCall(1) { 54 | func1(newCS) 55 | } 56 | if newCS.canCall(2) { 57 | func2(newCS) 58 | } 59 | if newCS.canCall(3) { 60 | func3(newCS) 61 | } 62 | if newCS.canCall(4) { 63 | func4(newCS) 64 | } 65 | if newCS.canCall(5) { 66 | func5(newCS) 67 | } 68 | if newCS.canCall(6) { 69 | func6(newCS) 70 | } 71 | if newCS.canCall(7) { 72 | func7(newCS) 73 | } 74 | } 75 | 76 | func func1(cs callStack) { 77 | data := make([]byte, allocSize) 78 | keepAlive = append(keepAlive, data) 79 | 80 | newCS := cs.markCalled(1) 81 | 82 | // Call all other functions that haven't been called yet 83 | if newCS.canCall(0) { 84 | func0(newCS) 85 | } 86 | if newCS.canCall(2) { 87 | func2(newCS) 88 | } 89 | if newCS.canCall(3) { 90 | func3(newCS) 91 | } 92 | if newCS.canCall(4) { 93 | func4(newCS) 94 | } 95 | if newCS.canCall(5) { 96 | func5(newCS) 97 | } 98 | if newCS.canCall(6) { 99 | func6(newCS) 100 | } 101 | if newCS.canCall(7) { 102 | func7(newCS) 103 | } 104 | } 105 | 106 | func func2(cs callStack) { 107 | data := make([]byte, allocSize) 108 | keepAlive = append(keepAlive, data) 109 | 110 | newCS := cs.markCalled(2) 111 | 112 | // Call all other functions that haven't been called yet 113 | if newCS.canCall(0) { 114 | func0(newCS) 115 | } 116 | if newCS.canCall(1) { 117 | func1(newCS) 118 | } 119 | if newCS.canCall(3) { 120 | func3(newCS) 121 | } 122 | if newCS.canCall(4) { 123 | func4(newCS) 124 | } 125 | if newCS.canCall(5) { 126 | func5(newCS) 127 | } 128 | if newCS.canCall(6) { 129 | func6(newCS) 130 | } 131 | if newCS.canCall(7) { 132 | func7(newCS) 133 | } 134 | } 135 | 136 | func func3(cs callStack) { 137 | data := make([]byte, allocSize) 138 | keepAlive = append(keepAlive, data) 139 | 140 | newCS := cs.markCalled(3) 141 | 142 | // Call all other functions that haven't been called yet 143 | if newCS.canCall(0) { 144 | func0(newCS) 145 | } 146 | if newCS.canCall(1) { 147 | func1(newCS) 148 | } 149 | if newCS.canCall(2) { 150 | func2(newCS) 151 | } 152 | if newCS.canCall(4) { 153 | func4(newCS) 154 | } 155 | if newCS.canCall(5) { 156 | func5(newCS) 157 | } 158 | if newCS.canCall(6) { 159 | func6(newCS) 160 | } 161 | if newCS.canCall(7) { 162 | func7(newCS) 163 | } 164 | } 165 | 166 | func func4(cs callStack) { 167 | data := make([]byte, allocSize) 168 | keepAlive = append(keepAlive, data) 169 | 170 | newCS := cs.markCalled(4) 171 | 172 | // Call all other functions that haven't been called yet 173 | if newCS.canCall(0) { 174 | func0(newCS) 175 | } 176 | if newCS.canCall(1) { 177 | func1(newCS) 178 | } 179 | if newCS.canCall(2) { 180 | func2(newCS) 181 | } 182 | if newCS.canCall(3) { 183 | func3(newCS) 184 | } 185 | if newCS.canCall(5) { 186 | func5(newCS) 187 | } 188 | if newCS.canCall(6) { 189 | func6(newCS) 190 | } 191 | if newCS.canCall(7) { 192 | func7(newCS) 193 | } 194 | } 195 | 196 | func func5(cs callStack) { 197 | // Allocate 1KB of memory 198 | data := make([]byte, allocSize) 199 | keepAlive = append(keepAlive, data) 200 | 201 | newCS := cs.markCalled(5) 202 | 203 | // Call all other functions that haven't been called yet 204 | if newCS.canCall(0) { 205 | func0(newCS) 206 | } 207 | if newCS.canCall(1) { 208 | func1(newCS) 209 | } 210 | if newCS.canCall(2) { 211 | func2(newCS) 212 | } 213 | if newCS.canCall(3) { 214 | func3(newCS) 215 | } 216 | if newCS.canCall(4) { 217 | func4(newCS) 218 | } 219 | if newCS.canCall(6) { 220 | func6(newCS) 221 | } 222 | if newCS.canCall(7) { 223 | func7(newCS) 224 | } 225 | } 226 | 227 | func func6(cs callStack) { 228 | // Allocate 1KB of memory 229 | data := make([]byte, allocSize) 230 | keepAlive = append(keepAlive, data) 231 | 232 | newCS := cs.markCalled(6) 233 | 234 | // Call all other functions that haven't been called yet 235 | if newCS.canCall(0) { 236 | func0(newCS) 237 | } 238 | if newCS.canCall(1) { 239 | func1(newCS) 240 | } 241 | if newCS.canCall(2) { 242 | func2(newCS) 243 | } 244 | if newCS.canCall(3) { 245 | func3(newCS) 246 | } 247 | if newCS.canCall(4) { 248 | func4(newCS) 249 | } 250 | if newCS.canCall(5) { 251 | func5(newCS) 252 | } 253 | if newCS.canCall(7) { 254 | func7(newCS) 255 | } 256 | } 257 | 258 | func func7(cs callStack) { 259 | // Allocate 1KB of memory 260 | data := make([]byte, allocSize) 261 | keepAlive = append(keepAlive, data) 262 | 263 | newCS := cs.markCalled(6) 264 | 265 | // Call all other functions that haven't been called yet 266 | if newCS.canCall(0) { 267 | func0(newCS) 268 | } 269 | if newCS.canCall(1) { 270 | func1(newCS) 271 | } 272 | if newCS.canCall(2) { 273 | func2(newCS) 274 | } 275 | if newCS.canCall(3) { 276 | func3(newCS) 277 | } 278 | if newCS.canCall(4) { 279 | func4(newCS) 280 | } 281 | if newCS.canCall(5) { 282 | func5(newCS) 283 | } 284 | if newCS.canCall(6) { 285 | func6(newCS) 286 | } 287 | } 288 | 289 | func main() { 290 | // Enable memory profiling and set profile rate to 1 to capture all allocations 291 | runtime.MemProfileRate = 1 292 | runtime.MemProfile(nil, false) 293 | 294 | fmt.Printf("Starting deep stack allocation test...\n") 295 | fmt.Printf("PID: %d\n", os.Getpid()) 296 | 297 | var cs callStack 298 | 299 | // Start from each function to create different initial call paths 300 | func0(cs) 301 | func1(cs) 302 | func2(cs) 303 | func3(cs) 304 | func4(cs) 305 | func5(cs) 306 | func6(cs) 307 | 308 | bind := ":8888" 309 | log.Println("Starting HTTP server on", bind) 310 | mux := http.NewServeMux() 311 | mux.HandleFunc("/debug/pprof/", pprof.Index) 312 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 313 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 314 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 315 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 316 | go func() { log.Fatal(http.ListenAndServe(bind, mux)) }() 317 | 318 | fmt.Printf("Allocated %d chunks\n", len(keepAlive)) 319 | fmt.Printf("Total memory allocated: %d bytes\n", len(keepAlive)*allocSize) 320 | 321 | // Keep the program running so we can profile it 322 | fmt.Printf("Program running, use 'sudo ./oompa -p %d' to profile\n", os.Getpid()) 323 | fmt.Println("Press Ctrl+C to exit...") 324 | 325 | // Sleep indefinitely 326 | for { 327 | time.Sleep(time.Hour) 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /tests/gccache/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Parca Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "flag" 18 | "fmt" 19 | "math/rand" 20 | "runtime" 21 | "time" 22 | ) 23 | 24 | // CacheEntry represents an entry in our leaky cache 25 | type CacheEntry struct { 26 | ID int 27 | Data []byte 28 | Refs []*CacheEntry // References to other entries (creates cycles) 29 | Created time.Time 30 | LastUsed time.Time 31 | } 32 | 33 | // LeakyCache simulates a cache that leaks memory 34 | type LeakyCache struct { 35 | entries map[int]*CacheEntry 36 | nextID int 37 | } 38 | 39 | func NewLeakyCache() *LeakyCache { 40 | return &LeakyCache{ 41 | entries: make(map[int]*CacheEntry), 42 | nextID: 1, 43 | } 44 | } 45 | 46 | // Add creates a new entry with references to existing entries 47 | func (c *LeakyCache) Add(dataSize int, numRefs int) { 48 | entry := &CacheEntry{ 49 | ID: c.nextID, 50 | Data: make([]byte, dataSize), 51 | Created: time.Now(), 52 | LastUsed: time.Now(), 53 | Refs: make([]*CacheEntry, 0, numRefs), 54 | } 55 | 56 | // Fill data with random bytes to prevent deduplication 57 | rand.Read(entry.Data) 58 | 59 | // Create references to random existing entries (cycles) 60 | if len(c.entries) > 0 { 61 | for i := 0; i < numRefs && i < len(c.entries); i++ { 62 | // Pick a random entry to reference 63 | randomID := rand.Intn(len(c.entries)) + 1 64 | if ref, exists := c.entries[randomID]; exists { 65 | entry.Refs = append(entry.Refs, ref) 66 | } 67 | } 68 | } 69 | 70 | c.entries[c.nextID] = entry 71 | c.nextID++ 72 | } 73 | 74 | // ExpireOld removes entries older than maxAge (but keeps most due to refs) 75 | func (c *LeakyCache) ExpireOld(maxAge time.Duration) int { 76 | now := time.Now() 77 | expired := 0 78 | 79 | // Only expire a fraction of eligible entries (simulating pinned entries) 80 | for id, entry := range c.entries { 81 | if now.Sub(entry.Created) > maxAge && rand.Float32() < 0.1 { 82 | delete(c.entries, id) 83 | expired++ 84 | } 85 | } 86 | 87 | return expired 88 | } 89 | 90 | // Touch updates the last used time of random entries 91 | func (c *LeakyCache) Touch(count int) { 92 | now := time.Now() 93 | touched := 0 94 | 95 | for id := range c.entries { 96 | if touched >= count { 97 | break 98 | } 99 | c.entries[id].LastUsed = now 100 | touched++ 101 | 102 | // Also touch referenced entries (increases memory scanning) 103 | for _, ref := range c.entries[id].Refs { 104 | ref.LastUsed = now 105 | } 106 | } 107 | } 108 | 109 | func main() { 110 | // Enable memory profiling for this process 111 | runtime.MemProfile(nil, false) 112 | runtime.MemProfileRate = 1 // Force profiling every allocation 113 | 114 | // Busy sleep for 5 seconds in 1ms chunks 115 | start := time.Now() 116 | for time.Since(start) < 15*time.Second { 117 | time.Sleep(0) 118 | } 119 | 120 | var entrySize int 121 | var addRate int 122 | var expireRate int 123 | 124 | flag.IntVar(&entrySize, "entry-size", 4096, "Size of each cache entry in bytes") 125 | flag.IntVar(&addRate, "add-rate", 1000, "Number of entries to add per iteration") 126 | flag.IntVar(&expireRate, "expire-rate", 100, "Number of entries to try to expire per iteration") 127 | flag.Parse() 128 | 129 | fmt.Printf("Starting GC cache test:\n") 130 | fmt.Printf("- Entry size: %d bytes\n", entrySize) 131 | fmt.Printf("- Add rate: %d entries/iteration\n", addRate) 132 | fmt.Printf("- Expire rate: %d entries/iteration\n", expireRate) 133 | 134 | cache := NewLeakyCache() 135 | iteration := 0 136 | 137 | // Force GC to run frequently 138 | runtime.GC() 139 | 140 | for { 141 | iteration++ 142 | 143 | // Add new entries with cross-references 144 | for i := 0; i < addRate; i++ { 145 | numRefs := rand.Intn(5) + 1 // 1-5 references to other entries 146 | cache.Add(entrySize, numRefs) 147 | } 148 | 149 | // Touch some random entries (simulating cache hits) 150 | cache.Touch(addRate / 10) 151 | 152 | // Try to expire old entries (but most won't be expired due to refs) 153 | expired := cache.ExpireOld(5 * time.Second) 154 | 155 | // Print stats every 100 iterations 156 | if iteration%100 == 0 { 157 | var m runtime.MemStats 158 | runtime.ReadMemStats(&m) 159 | 160 | fmt.Printf("Iteration %d: %d entries, %d expired, Alloc: %d MB, TotalAlloc: %d MB, GC: %d\n", 161 | iteration, len(cache.entries), expired, 162 | m.Alloc/1024/1024, m.TotalAlloc/1024/1024, m.NumGC) 163 | 164 | // Force GC to increase pressure 165 | runtime.GC() 166 | } 167 | 168 | // Small sleep to allow GC to run 169 | time.Sleep(1 * time.Millisecond) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/oomer/main.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | // Copyright 2022-2025 The Parca Authors 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | "runtime" 22 | "runtime/debug" 23 | "strings" 24 | "time" 25 | 26 | _ "github.com/KimMachineGun/automemlimit" 27 | ) 28 | 29 | func main() { 30 | // Enable memory profiling for this process 31 | runtime.MemProfile(nil, false) 32 | runtime.MemProfileRate = 1 // Force profiling every allocation 33 | 34 | // Busy sleep for 5 seconds in 1ms chunks 35 | start := time.Now() 36 | for time.Since(start) < 15*time.Second { 37 | time.Sleep(0) 38 | } 39 | 40 | fmt.Printf("oomer %d\n", os.Getpid()) 41 | checkSwap() 42 | if len(os.Args) > 1 && os.Args[1] == "--many" { 43 | allocSpace() 44 | } 45 | i := debug.SetMemoryLimit(-1) 46 | bigAlloc(i * 2) 47 | } 48 | 49 | type mem struct { 50 | space []byte 51 | children []*mem 52 | } 53 | 54 | func allocSpace() { 55 | allocSpaceRecursive(10, 10) 56 | } 57 | 58 | func allocSpaceRecursive(depth, children int) *mem { 59 | // recursive function to allocate memory 60 | if depth <= 0 { 61 | return nil 62 | } 63 | // Allocate a slice of bytes 64 | var node mem 65 | 66 | node.space = make([]byte, os.Getpagesize()) 67 | node.space[0] = 0xFF // touch the page to ensure it's allocated 68 | for i := 0; i < children; i++ { 69 | node.children = append(node.children, allocSpaceRecursive(depth-1, children)) 70 | } 71 | return &node 72 | } 73 | 74 | func checkSwap() { 75 | content, err := os.ReadFile("/proc/swaps") 76 | if err != nil { 77 | panic("Could not read /proc/swaps") 78 | } 79 | 80 | lines := strings.Split(string(content), "\n") 81 | // If there's more than just the header line, swap is configured 82 | if len(lines) > 1 && len(lines[1]) > 0 { 83 | panic("swap enabled, disable it") 84 | } 85 | 86 | } 87 | 88 | func bigAlloc(i int64) { 89 | var space []byte 90 | 91 | // Try to allocate memory, reducing size if allocation fails 92 | for space == nil && i > 0 { 93 | // Use defer and recover to handle potential OOM errors 94 | func() { 95 | defer func() { 96 | if r := recover(); r != nil { 97 | fmt.Printf("Failed to allocate %d bytes, trying a smaller size\n", i) 98 | i = i - 4096 99 | } 100 | }() 101 | 102 | // Try to allocate memory 103 | space = make([]byte, i) 104 | }() 105 | } 106 | 107 | if space == nil { 108 | fmt.Println("Could not allocate memory") 109 | return 110 | } 111 | 112 | fmt.Printf("Successfully allocated %d bytes\n", i) 113 | 114 | // Actually use the memory by writing to it 115 | for k := int64(0); k < i; k += 4096 { 116 | space[k] = byte(k & 255) 117 | } 118 | 119 | fmt.Println("Memory filled. Should have oomed, going for more ...") 120 | bigAlloc(i / 2) 121 | fmt.Printf("Successfully allocated %d bytes\n", i) 122 | } 123 | -------------------------------------------------------------------------------- /tests/parca-agent-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Integration test for OOM profiling with parca-agent and Parca server 5 | # This script: 6 | # 1. Starts Parca server in Docker 7 | # 2. Starts parca-agent (Docker or local build) 8 | # 3. Runs memory-limited tests that should trigger OOM profiles 9 | # 4. Validates that OOM profiles are received by Parca server 10 | 11 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 12 | OOMPROF_ROOT="$(dirname "$SCRIPT_DIR")" 13 | PARCA_AGENT_DIR="${PARCA_AGENT_DIR:-../parca-agent}" 14 | 15 | # Configuration 16 | PARCA_PORT="${PARCA_PORT:-7070}" 17 | PARCA_CONTAINER_NAME="parca" 18 | AGENT_CONTAINER_NAME="oomprof-test-agent" 19 | TEST_TIMEOUT="${TEST_TIMEOUT:-300}" # 5 minutes 20 | USE_LOCAL_AGENT="${USE_LOCAL_AGENT:-false}" 21 | USE_EXISTING_PARCA="${USE_EXISTING_PARCA:-false}" 22 | DRY_RUN="${DRY_RUN:-false}" 23 | PARCA_AGENT_REV="${PARCA_AGENT_REV:-v0.39.3}" # Default to a specific version 24 | PARALLEL_TESTS=1 # Default to sequential execution 25 | 26 | # Colors for output 27 | RED='\033[0;31m' 28 | GREEN='\033[0;32m' 29 | YELLOW='\033[1;33m' 30 | BLUE='\033[0;34m' 31 | NC='\033[0m' # No Color 32 | 33 | log() { 34 | echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1" 35 | } 36 | 37 | warn() { 38 | echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARNING:${NC} $1" 39 | } 40 | 41 | error() { 42 | echo -e "${RED}[$(date '+%H:%M:%S')] ERROR:${NC} $1" 43 | } 44 | 45 | success() { 46 | echo -e "${GREEN}[$(date '+%H:%M:%S')] SUCCESS:${NC} $1" 47 | } 48 | 49 | cleanup() { 50 | log "Cleaning up containers and resources..." 51 | 52 | # We leave things running for inspection. 53 | # stop() 54 | } 55 | 56 | stop() { 57 | # Stop and remove containers 58 | docker stop "$PARCA_CONTAINER_NAME" 2>/dev/null || true 59 | docker rm "$PARCA_CONTAINER_NAME" 2>/dev/null || true 60 | docker stop "$AGENT_CONTAINER_NAME" 2>/dev/null || true 61 | docker rm "$AGENT_CONTAINER_NAME" 2>/dev/null || true 62 | 63 | 64 | # Kill any local parca-agent processes 65 | if [ "$USE_LOCAL_AGENT" = "true" ]; then 66 | sudo killall parca-agent 2>/dev/null || true 67 | fi 68 | } 69 | 70 | # Set up cleanup trap 71 | trap cleanup EXIT 72 | 73 | check_docker() { 74 | if ! command -v docker &> /dev/null; then 75 | error "Docker is required but not installed" 76 | exit 1 77 | fi 78 | 79 | if ! docker info &> /dev/null; then 80 | error "Docker daemon is not running" 81 | exit 1 82 | fi 83 | } 84 | 85 | wait_for_service() { 86 | local service_name="$1" 87 | local host="$2" 88 | local port="$3" 89 | local timeout="$4" 90 | 91 | log "Waiting for $service_name to be ready at $host:$port..." 92 | 93 | local count=0 94 | while [ $count -lt $timeout ]; do 95 | if curl -s --connect-timeout 1 "http://$host:$port" > /dev/null 2>&1; then 96 | success "$service_name is ready!" 97 | return 0 98 | fi 99 | sleep 1 100 | count=$((count + 1)) 101 | if [ $((count % 10)) -eq 0 ]; then 102 | log "Still waiting for $service_name... ($count/$timeout seconds)" 103 | fi 104 | done 105 | 106 | error "$service_name failed to start within $timeout seconds" 107 | return 1 108 | } 109 | 110 | check_existing_parca() { 111 | # Check if port is already in use 112 | if lsof -i :"$PARCA_PORT" >/dev/null 2>&1; then 113 | log "Port $PARCA_PORT is already in use" 114 | 115 | # Check if it's a Parca container 116 | if docker ps | grep -q "parca.*$PARCA_PORT"; then 117 | local existing_container=$(docker ps | grep "parca.*$PARCA_PORT" | awk '{print $NF}') 118 | warn "Found existing Parca server container: $existing_container" 119 | 120 | if [ "$USE_EXISTING_PARCA" = "true" ]; then 121 | log "Using existing Parca server (USE_EXISTING_PARCA=true)" 122 | return 0 123 | else 124 | error "Port $PARCA_PORT is already in use by Parca server" 125 | error "Either stop the existing server or set USE_EXISTING_PARCA=true" 126 | exit 1 127 | fi 128 | else 129 | error "Port $PARCA_PORT is already in use by another process" 130 | exit 1 131 | fi 132 | fi 133 | return 1 134 | } 135 | 136 | start_parca_server() { 137 | log "Starting Parca server..." 138 | 139 | # Check if we should use existing server 140 | if check_existing_parca; then 141 | return 0 142 | fi 143 | 144 | # Clean up old container 145 | docker stop "$PARCA_CONTAINER_NAME" 2>/dev/null || true 146 | docker rm "$PARCA_CONTAINER_NAME" 2>/dev/null || true 147 | 148 | # Start Parca server 149 | docker run -d \ 150 | --name "$PARCA_CONTAINER_NAME" \ 151 | --privileged \ 152 | -p "$PARCA_PORT:7070" \ 153 | ghcr.io/parca-dev/parca:v0.24.0 \ 154 | /parca \ 155 | --log-level=debug 156 | 157 | # Wait for Parca to be ready 158 | wait_for_service "Parca server" "localhost" "$PARCA_PORT" 60 159 | 160 | # Verify gRPC endpoint 161 | log "Verifying Parca gRPC endpoint..." 162 | if ! timeout 10 bash -c "echo > /dev/tcp/localhost/$PARCA_PORT" 2>/dev/null; then 163 | error "Parca gRPC endpoint not accessible" 164 | exit 1 165 | fi 166 | 167 | success "Parca server started successfully" 168 | } 169 | 170 | build_local_agent() { 171 | log "Building local parca-agent..." 172 | 173 | if [ ! -d "$PARCA_AGENT_DIR" ]; then 174 | error "Parca agent directory not found: $PARCA_AGENT_DIR" 175 | error "Set PARCA_AGENT_DIR environment variable to the correct path" 176 | exit 1 177 | fi 178 | 179 | cd "$PARCA_AGENT_DIR" 180 | 181 | # Check if Makefile exists 182 | if [ ! -f "Makefile" ]; then 183 | error "Makefile not found in $PARCA_AGENT_DIR" 184 | exit 1 185 | fi 186 | 187 | # Build the agent 188 | log "Running 'make build' in $PARCA_AGENT_DIR..." 189 | make build 190 | 191 | if [ ! -f "./parca-agent" ]; then 192 | error "parca-agent binary not found after build" 193 | exit 1 194 | fi 195 | 196 | success "parca-agent built successfully" 197 | cd - 198 | } 199 | 200 | start_local_agent() { 201 | log "Starting local parca-agent..." 202 | 203 | cd "$PARCA_AGENT_DIR" 204 | 205 | # Start parca-agent with OOM profiling enabled 206 | log "Starting parca-agent with OOM profiling..." 207 | sudo ./parca-agent \ 208 | --node="test-node" \ 209 | --remote-store-address="localhost:$PARCA_PORT" \ 210 | --remote-store-insecure \ 211 | --enable-oom-prof \ 212 | --config-path="$SCRIPT_DIR/parca-agent.yml" \ 213 | 2>&1 | tee -i "/tmp/parca-agent.log" & 214 | 215 | local agent_pid=$! 216 | echo $agent_pid > "/tmp/parca-agent.pid" 217 | 218 | # Give agent time to start 219 | sleep 5 220 | 221 | # Check if agent is still running 222 | if ! kill -0 $agent_pid 2>/dev/null; then 223 | error "parca-agent failed to start" 224 | log "Agent log output:" 225 | cat "/tmp/parca-agent.log" || true 226 | exit 1 227 | fi 228 | 229 | success "Local parca-agent started with PID $agent_pid" 230 | cd - 231 | } 232 | 233 | start_docker_agent() { 234 | log "Starting parca-agent in Docker..." 235 | 236 | if docker ps | grep -q "$AGENT_CONTAINER_NAME"; then 237 | log "parca-agent container is already running" 238 | return 0 239 | fi 240 | 241 | # Start parca-agent container 242 | docker run -d \ 243 | --name "$AGENT_CONTAINER_NAME" \ 244 | --privileged \ 245 | --pid=host \ 246 | --network=host \ 247 | -v /sys/kernel/debug:/sys/kernel/debug:rw \ 248 | -v /lib/modules:/lib/modules:ro \ 249 | -v /usr/src:/usr/src:ro \ 250 | -v /etc/machine-id:/etc/machine-id:ro \ 251 | -v "$SCRIPT_DIR/parca-agent.yml:/parca-agent.yml:ro" \ 252 | ghcr.io/parca-dev/parca-agent:$PARCA_AGENT_REV \ 253 | --node="test-node" \ 254 | --remote-store-address="localhost:$PARCA_PORT" \ 255 | --remote-store-insecure \ 256 | --enable-oom-prof \ 257 | --config-path="/parca-agent.yml" 258 | 259 | # Give agent time to start 260 | sleep 10 261 | 262 | # Check if container is running 263 | if ! docker ps | grep -q "$AGENT_CONTAINER_NAME"; then 264 | error "parca-agent container failed to start" 265 | log "Container logs:" 266 | docker logs "$AGENT_CONTAINER_NAME" 2>/dev/null || true 267 | exit 1 268 | fi 269 | 270 | success "Docker parca-agent started successfully" 271 | } 272 | 273 | run_memory_tests() { 274 | log "Running memory-limited tests..." 275 | 276 | cd "$SCRIPT_DIR" 277 | 278 | # Check if the test script exists 279 | if [ ! -f "./run-all-memlimited.sh" ]; then 280 | error "Memory test script not found: ./run-all-memlimited.sh" 281 | exit 1 282 | fi 283 | 284 | # Make sure it's executable 285 | chmod +x "./run-all-memlimited.sh" 286 | 287 | if [ "$PARALLEL_TESTS" -gt 1 ]; then 288 | log "Executing $PARALLEL_TESTS memory-limited tests in parallel..." 289 | run_parallel_memory_tests 290 | else 291 | log "Executing memory-limited tests that should trigger OOM events..." 292 | 293 | # Run the tests with a timeout 294 | if timeout $TEST_TIMEOUT ./run-all-memlimited.sh; then 295 | success "Memory tests completed successfully" 296 | else 297 | local exit_code=$? 298 | if [ $exit_code -eq 124 ]; then 299 | warn "Memory tests timed out after $TEST_TIMEOUT seconds" 300 | else 301 | warn "Memory tests exited with code $exit_code (may be expected for OOM tests)" 302 | fi 303 | fi 304 | fi 305 | 306 | # Give time for profiles to be processed 307 | log "Waiting for profiles to be processed and sent to Parca..." 308 | sleep 10 309 | } 310 | 311 | run_parallel_memory_tests() { 312 | local pids=() 313 | local failed=0 314 | 315 | # Create a temporary directory for parallel test logs 316 | local parallel_log_dir="/tmp/parca-parallel-tests-$$" 317 | mkdir -p "$parallel_log_dir" 318 | 319 | log "Starting $PARALLEL_TESTS parallel test instances..." 320 | 321 | for i in $(seq 1 "$PARALLEL_TESTS"); do 322 | log "Starting test instance $i..." 323 | 324 | # Run each test instance in background with unique log 325 | ( 326 | # Each instance gets its own cgroup to avoid conflicts 327 | export CGROUP_NAME="oomprof-test-$$-$i" 328 | timeout $TEST_TIMEOUT ./run-all-memlimited.sh > "$parallel_log_dir/test-$i.log" 2>&1 329 | echo $? > "$parallel_log_dir/test-$i.exitcode" 330 | ) & 331 | 332 | pids+=($!) 333 | 334 | # Small delay between starts to avoid race conditions 335 | sleep 0.5 336 | done 337 | 338 | log "Waiting for all test instances to complete..." 339 | 340 | # Wait for all background processes 341 | for i in "${!pids[@]}"; do 342 | local pid=${pids[$i]} 343 | local instance=$((i + 1)) 344 | 345 | wait $pid 346 | local exit_code=$(cat "$parallel_log_dir/test-$instance.exitcode" 2>/dev/null || echo "255") 347 | 348 | if [ $exit_code -eq 0 ]; then 349 | success "Test instance $instance completed successfully" 350 | elif [ $exit_code -eq 124 ]; then 351 | warn "Test instance $instance timed out after $TEST_TIMEOUT seconds" 352 | ((failed++)) 353 | else 354 | warn "Test instance $instance exited with code $exit_code (may be expected for OOM tests)" 355 | fi 356 | 357 | # Show last few lines of log for debugging 358 | if [ -f "$parallel_log_dir/test-$instance.log" ]; then 359 | log "Last 5 lines from test instance $instance:" 360 | tail -5 "$parallel_log_dir/test-$instance.log" | sed 's/^/ /' 361 | fi 362 | done 363 | 364 | # Cleanup 365 | rm -rf "$parallel_log_dir" 366 | 367 | if [ $failed -gt 0 ]; then 368 | warn "$failed test instances had issues, but this may be expected for OOM tests" 369 | else 370 | success "All $PARALLEL_TESTS test instances completed" 371 | fi 372 | } 373 | 374 | query_parca_profiles() { 375 | local query="$1" 376 | local start_time="$2" 377 | local end_time="$3" 378 | 379 | log "Querying Parca for profiles: $query" 380 | 381 | # Use grpcurl to query Parca's gRPC API 382 | local response 383 | response=$(grpcurl -plaintext \ 384 | -d "{\"query\": \"$query\", \"start\": \"$start_time\", \"end\": \"$end_time\"}" \ 385 | localhost:$PARCA_PORT \ 386 | parca.query.v1alpha1.QueryService/QueryRange 2>/dev/null || echo "{}") 387 | 388 | echo "$response" 389 | } 390 | 391 | validate_oom_profiles() { 392 | log "Validating OOM profiles in Parca..." 393 | 394 | # Calculate time range (last 30 minutes to be sure we catch profiles) 395 | local end_time=$(date -u +%Y-%m-%dT%H:%M:%SZ) 396 | local start_time=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ) 397 | 398 | log "Searching for profiles between $start_time and $end_time" 399 | 400 | # Wait a bit more for profiles to be processed 401 | sleep 15 402 | 403 | # Query for all memory profiles (label queries have syntax issues, so just check for any memory profiles) 404 | log "Querying for all memory profiles..." 405 | local all_memory_response 406 | all_memory_response=$(query_parca_profiles 'memory:inuse_space:bytes:space:bytes' "$start_time" "$end_time") 407 | 408 | # Check if we got any profiles by looking for series data 409 | local total_count=0 410 | 411 | # Count series in the grpcurl JSON response - each series represents a different process/profile 412 | if echo "$all_memory_response" | grep -q '"series"'; then 413 | total_count=$(echo "$all_memory_response" | grep -o '"labelset"' | wc -l) 414 | fi 415 | 416 | log "Profile query results:" 417 | log " - Total memory profiles found: $total_count" 418 | 419 | # Show what profiles we found 420 | if [ "$total_count" -gt 0 ]; then 421 | log "Found memory profiles from processes:" 422 | echo "$all_memory_response" | grep -o '"comm",[^}]*"value":"[^"]*"' | sed 's/.*"value":"\([^"]*\)".*/ - \1/' || echo " (could not parse process names)" 423 | fi 424 | 425 | # Also check available profile types 426 | log "Checking available profile types..." 427 | local types_response 428 | types_response=$(grpcurl -plaintext localhost:$PARCA_GRPC_PORT parca.query.v1alpha1.QueryService/ProfileTypes 2>/dev/null || echo "{}") 429 | if echo "$types_response" | grep -q "memory"; then 430 | log "Memory profile type is available in Parca" 431 | fi 432 | 433 | # Check available labels 434 | log "Checking available labels..." 435 | local labels_response 436 | labels_response=$(grpcurl -plaintext localhost:$PARCA_GRPC_PORT parca.query.v1alpha1.QueryService/Labels 2>/dev/null || echo "{}") 437 | log "Available labels: $(echo "$labels_response" | grep -o '"name":[^,]*' | head -5 || echo "none found")" 438 | 439 | # Check Parca logs for profile storage activity as a fallback 440 | log "Checking Parca server logs for profile storage activity..." 441 | local profile_writes=$(docker logs "$PARCA_CONTAINER_NAME" 2>&1 | grep -c "grpc.service=parca.profilestore.v1alpha1.ProfileStoreService grpc.method=WriteRaw" || echo "0") 442 | log "Found $profile_writes profile write operations in Parca logs" 443 | 444 | # Validate results - we need at least 2 profiles per parallel test instance 445 | local validation_passed=false 446 | local expected_min_profiles=$((2 * PARALLEL_TESTS)) 447 | 448 | if [ "$total_count" -ge "$expected_min_profiles" ]; then 449 | success "Found $total_count memory profiles! OOM profiling integration test PASSED!" 450 | validation_passed=true 451 | else 452 | # Fallback to checking Parca logs for profile activity 453 | if [ "$profile_writes" -ge "$expected_min_profiles" ]; then 454 | success "Found $profile_writes profile write operations in Parca logs! OOM profiling integration test PASSED!" 455 | validation_passed=true 456 | else 457 | error "Expected at least $expected_min_profiles memory profiles (2 per test instance x $PARALLEL_TESTS instances), but found:" 458 | error " - Total memory profiles: $total_count" 459 | error " - Profile write operations: $profile_writes" 460 | 461 | # Show sample response for debugging 462 | log "Sample query response:" 463 | echo "$all_memory_response" | head -20 464 | fi 465 | fi 466 | 467 | if [ "$validation_passed" = true ]; then 468 | return 0 469 | else 470 | return 1 471 | fi 472 | } 473 | 474 | main() { 475 | log "Starting OOM profiling integration test..." 476 | log "Configuration:" 477 | log " - Parca HTTP port: $PARCA_HTTP_PORT" 478 | log " - Parca gRPC port: $PARCA_GRPC_PORT" 479 | log " - Use local agent: $USE_LOCAL_AGENT" 480 | log " - Parca agent dir: $PARCA_AGENT_DIR" 481 | log " - Test timeout: $TEST_TIMEOUT seconds" 482 | log " - Parallel tests: $PARALLEL_TESTS" 483 | log " - Dry run: $DRY_RUN" 484 | 485 | if [ "$DRY_RUN" = "true" ]; then 486 | log "DRY RUN MODE - No actual containers will be started" 487 | log "This mode shows what the test would do without executing commands" 488 | log "" 489 | fi 490 | 491 | # Prerequisites 492 | check_docker 493 | 494 | #stop 495 | 496 | # Start Parca server 497 | start_parca_server 498 | 499 | # Start parca-agent 500 | if [ "$USE_LOCAL_AGENT" = "true" ]; then 501 | build_local_agent 502 | start_local_agent 503 | else 504 | start_docker_agent 505 | fi 506 | 507 | # Run memory tests that should trigger OOM events 508 | run_memory_tests 509 | 510 | # Validate that profiles were received 511 | if validate_oom_profiles; then 512 | success "Integration test PASSED! OOM profiles were successfully received by Parca." 513 | else 514 | error "Integration test FAILED! Expected OOM profiles were not found in Parca." 515 | exit 1 516 | fi 517 | 518 | stop 519 | } 520 | 521 | # Parse command line arguments 522 | while [[ $# -gt 0 ]]; do 523 | case $1 in 524 | -n|--parallel) 525 | PARALLEL_TESTS="$2" 526 | if ! [[ "$PARALLEL_TESTS" =~ ^[0-9]+$ ]] || [ "$PARALLEL_TESTS" -lt 1 ]; then 527 | error "Invalid value for -n/--parallel: must be a positive integer" 528 | exit 1 529 | fi 530 | shift 2 531 | ;; 532 | -h|--help) 533 | echo "OOM Profiling Integration Test" 534 | echo "" 535 | echo "This script tests the full integration between oomprof, parca-agent, and Parca server." 536 | echo "" 537 | echo "Usage: $0 [options]" 538 | echo "" 539 | echo "Options:" 540 | echo " -n, --parallel Run NUMBER of memory tests in parallel (default: 1)" 541 | echo " -h, --help Show this help message" 542 | echo "" 543 | echo "Environment Variables:" 544 | echo " USE_LOCAL_AGENT=true Use local parca-agent build instead of Docker" 545 | echo " PARCA_AGENT_DIR=/path Path to parca-agent source directory (default: ../parca-agent)" 546 | echo " PARCA_HTTP_PORT=7071 Parca HTTP port (default: 7071)" 547 | echo " PARCA_GRPC_PORT=7070 Parca gRPC port (default: 7070)" 548 | echo " TEST_TIMEOUT=300 Test timeout in seconds (default: 300)" 549 | echo " DRY_RUN=true Show what would be done without executing" 550 | echo "" 551 | echo "Examples:" 552 | echo " # Run with Docker agent:" 553 | echo " sudo $0" 554 | echo "" 555 | echo " # Run with local agent build:" 556 | echo " sudo USE_LOCAL_AGENT=true PARCA_AGENT_DIR=/path/to/parca-agent $0" 557 | echo "" 558 | echo " # Run 4 memory tests in parallel:" 559 | echo " sudo $0 -n 4" 560 | echo "" 561 | echo "Requirements:" 562 | echo " - Root privileges (for eBPF)" 563 | echo " - Docker and Docker daemon running" 564 | echo " - Internet connection (to pull Docker images)" 565 | echo " - Available ports $PARCA_PORT" 566 | exit 0 567 | ;; 568 | *) 569 | error "Unknown option: $1" 570 | echo "Use -h or --help for usage information" 571 | exit 1 572 | ;; 573 | esac 574 | done 575 | 576 | # Run main function 577 | main "$@" -------------------------------------------------------------------------------- /tests/run-all-memlimited.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to run all Go programs under tests/ in a 0.5GB memory limited cgroup 4 | # Requires root or appropriate cgroup permissions 5 | 6 | set -e 7 | 8 | # Memory limit in bytes (0.5GB = 512MB) 9 | MEMORY_LIMIT=$((100 * 1024 * 1024)) 10 | # Use environment variable if set (for parallel execution), otherwise use default 11 | CGROUP_NAME="${CGROUP_NAME:-oomprof-test-$$}" 12 | CGROUP_PATH="/sys/fs/cgroup/${CGROUP_NAME}" 13 | 14 | # Colors for output 15 | RED='\033[0;31m' 16 | GREEN='\033[0;32m' 17 | YELLOW='\033[1;33m' 18 | NC='\033[0m' # No Color 19 | 20 | echo -e "${YELLOW}Setting up cgroup with 512MB memory limit...${NC}" 21 | 22 | # Create cgroup v2 directory 23 | if [ ! -d "$CGROUP_PATH" ]; then 24 | sudo mkdir -p "$CGROUP_PATH" 25 | fi 26 | 27 | # Set memory limit 28 | echo "$MEMORY_LIMIT" | sudo tee "${CGROUP_PATH}/memory.max" > /dev/null 29 | echo -e "${GREEN}Created cgroup: ${CGROUP_PATH}${NC}" 30 | echo -e "${GREEN}Memory limit: 512MB${NC}\n" 31 | 32 | # Function to run a Go program in the cgroup 33 | run_in_cgroup() { 34 | local name="$1" 35 | 36 | echo -e "${YELLOW}Running ${name}...${NC}" 37 | 38 | # Check if the .taux binary exists 39 | if [ -f "${name}.taux" ]; then 40 | echo "Executing ${name}.taux in memory-limited cgroup..." 41 | sudo bash ./run-in-cgroup.sh "/${CGROUP_NAME}" "./${name}.taux" 2>&1 | tee "${name}.log" || { 42 | echo -e "${RED}${name} failed (likely OOM)${NC}" 43 | } 44 | else 45 | echo -e "${RED}No ${name}.taux binary found. Run 'make tests' first.${NC}" 46 | fi 47 | 48 | echo -e "${GREEN}Completed ${name}${NC}\n" 49 | } 50 | 51 | # Build test binaries using Makefile 52 | echo -e "${YELLOW}Building test binaries with Makefile...${NC}" 53 | cd .. 54 | make tests 55 | cd tests 56 | echo -e "${GREEN}Test binaries built successfully${NC}\n" 57 | 58 | # Run all Go programs 59 | echo -e "${YELLOW}Running all Go programs with 512MB memory limit...${NC}\n" 60 | 61 | # Run each Go program 62 | #run_in_cgroup "compile-oom" 63 | #run_in_cgroup "deepstack" 64 | run_in_cgroup "gccache" 65 | run_in_cgroup "oomer" 66 | 67 | # Cleanup 68 | echo -e "${YELLOW}Cleaning up cgroup...${NC}" 69 | # First, ensure no processes are in the cgroup 70 | sudo sh -c "echo 0 > '${CGROUP_PATH}/cgroup.procs' 2>/dev/null || true" 71 | # Remove the cgroup 72 | sudo rmdir "$CGROUP_PATH" 2>/dev/null || { 73 | echo -e "${RED}Warning: Could not remove cgroup. It may still have processes.${NC}" 74 | echo "You can manually remove it with: sudo rmdir $CGROUP_PATH" 75 | } 76 | 77 | echo -e "${GREEN}All tests completed!${NC}" 78 | echo -e "${YELLOW}Check individual .log files in each directory for output.${NC}" -------------------------------------------------------------------------------- /tests/run-in-cgroup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to run a command within a specific cgroup 4 | # Usage: run-in-cgroup.sh [args...] 5 | 6 | set -e 7 | 8 | if [ $# -lt 2 ]; then 9 | echo "Usage: $0 [args...]" >&2 10 | exit 1 11 | fi 12 | 13 | CGROUP_PATH="$1" 14 | shift 15 | COMMAND="$1" 16 | shift 17 | 18 | # Move current process to the cgroup 19 | echo $$ > "/sys/fs/cgroup${CGROUP_PATH}/cgroup.procs" 20 | 21 | export GOTOOLCHAIN=$GOTOOLCHAIN 22 | export GODEBUG=$GODEBUG 23 | export GOMODCACHE=$GOMODECACHE 24 | 25 | "$COMMAND" "$@" -------------------------------------------------------------------------------- /tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Test the current package under a different kernel. 3 | # Requires qemu-system-$QEMU_ARCH and bluebox to be installed. 4 | 5 | set -eu 6 | set -o pipefail 7 | 8 | qemu_arch="${QEMU_ARCH:-x86_64}" 9 | color_green=$'\033[32m' 10 | color_red=$'\033[31m' 11 | color_default=$'\033[39m' 12 | 13 | # Use sudo if /dev/kvm isn't accessible by the current user. 14 | sudo="" 15 | if [[ ! -r /dev/kvm || ! -w /dev/kvm ]]; then 16 | sudo="sudo" 17 | fi 18 | readonly sudo 19 | 20 | readonly kernel_version="${1:-}" 21 | if [[ -z "${kernel_version}" ]]; then 22 | echo "Expecting kernel version as first argument" 23 | exit 1 24 | fi 25 | 26 | readonly output="test-${kernel_version}/" 27 | mkdir -p "${output}" 28 | readonly kern_dir="${KERN_DIR:-../ci-kernels}" 29 | 30 | test -e "${kern_dir}/${kernel_version}/vmlinuz" || { 31 | echo "Failed to find kernel image ${kern_dir}/${kernel_version}/vmlinuz." 32 | exit 1 33 | } 34 | 35 | echo Generating initramfs 36 | expected=0 37 | 38 | bb_args=(-o "${output}/initramfs.cpio") 39 | while IFS='' read -r -d '' line ; do 40 | bb_args+=(-e "${line}:-test.v") 41 | ((expected=expected+1)) 42 | done < <(find . -name '*.test' -print0) 43 | 44 | # Add all taux files and run-in-cgroup.sh with flat structure 45 | while IFS='' read -r -d '' line ; do 46 | bb_args+=(-r "${line}") 47 | done < <(find . -name '*.taux' -print0) 48 | 49 | bb_args+=(-r "run-in-cgroup.sh") 50 | 51 | additionalQemuArgs="" 52 | 53 | supportKVM=$(grep -E 'vmx|svm' /proc/cpuinfo || true) 54 | if [ ! "$supportKVM" ] && [ "$qemu_arch" = "$(uname -m)" ]; then 55 | additionalQemuArgs="-enable-kvm" 56 | fi 57 | 58 | case "$qemu_arch" in 59 | x86_64) 60 | additionalQemuArgs+=" -append console=ttyS0" 61 | bb_args+=(-a amd64) 62 | ;; 63 | aarch64) 64 | additionalQemuArgs+=" -machine virt -cpu max" 65 | bb_args+=(-a arm64) 66 | ;; 67 | esac 68 | 69 | if [ "$qemu_arch" = "aarch64" ]; then 70 | additionalQemuArgs+=" -machine virt -cpu max" 71 | fi 72 | 73 | echo bb_args: "${bb_args[@]}" 74 | bluebox "${bb_args[@]}" || (echo "failed to generate initramfs"; exit 1) 75 | 76 | echo Testing on "${kernel_version}" 77 | 78 | $sudo qemu-system-${qemu_arch} ${additionalQemuArgs} \ 79 | -nographic \ 80 | -monitor none \ 81 | -serial file:"${output}/test.log" \ 82 | -no-user-config \ 83 | -m 950M \ 84 | -kernel "${kern_dir}/${kernel_version}/vmlinuz" \ 85 | -initrd "${output}/initramfs.cpio" 86 | 87 | # Dump the output of the VM run. 88 | cat "${output}/test.log" 89 | 90 | # Qemu will produce an escape sequence that disables line-wrapping in the terminal, 91 | # end result being truncated output. This restores line-wrapping after the fact. 92 | tput smam || true 93 | 94 | passes=$(grep -c "stdout: PASS" "${output}/test.log") 95 | 96 | if [ "$passes" -ne "$expected" ]; then 97 | echo "Test ${color_red}failed${color_default} on ${kernel_version}" 98 | EXIT_CODE=1 99 | else 100 | echo "Test ${color_green}successful${color_default} on ${kernel_version}" 101 | EXIT_CODE=0 102 | fi 103 | 104 | # Keep output directory for inspection 105 | echo "Test output saved in ${output}" 106 | 107 | exit $EXIT_CODE 108 | --------------------------------------------------------------------------------