diff --git a/Makefile b/Makefile index 23b87d4ed..41499505f 100644 --- a/Makefile +++ b/Makefile @@ -30,8 +30,8 @@ endif export TARGET_ARCH export CGO_ENABLED = 0 export GOARCH = $(TARGET_ARCH) -export CC = $(ARCH_PREFIX)-linux-gnu-gcc -export OBJCOPY = $(ARCH_PREFIX)-linux-gnu-objcopy +# export CC = $(ARCH_PREFIX)-linux-gnu-gcc +# export OBJCOPY = $(ARCH_PREFIX)-linux-gnu-objcopy BRANCH = $(shell git rev-parse --abbrev-ref HEAD | tr -d '-' | tr '[:upper:]' '[:lower:]') COMMIT_SHORT_SHA = $(shell git rev-parse --short=8 HEAD) diff --git a/go.mod b/go.mod index e078ca348..9fc655a48 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.16 github.com/aws/aws-sdk-go-v2/service/s3 v1.100.0 github.com/cilium/ebpf v0.21.0 + github.com/docker/go-connections v0.6.0 github.com/elastic/go-freelru v0.16.0 github.com/elastic/go-perf v0.0.0-20260224073651-af0ee0c731b7 github.com/google/uuid v1.6.0 @@ -24,10 +25,10 @@ require ( github.com/open-telemetry/sig-profiling/profcheck v0.0.0-20260427172309-47cc6a9c3929 github.com/peterbourgon/ff/v3 v3.4.0 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 github.com/zeebo/xxh3 v1.1.0 go.opentelemetry.io/collector/component v1.56.0 go.opentelemetry.io/collector/component/componenttest v0.150.0 - go.opentelemetry.io/collector/confmap v1.56.0 go.opentelemetry.io/collector/confmap/xconfmap v0.150.0 go.opentelemetry.io/collector/consumer/consumertest v0.150.0 go.opentelemetry.io/collector/consumer/xconsumer v0.150.0 @@ -40,7 +41,6 @@ require ( go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/proto/otlp v1.10.0 go.opentelemetry.io/proto/otlp/profiles/v1development v0.3.0 - go.uber.org/goleak v1.3.0 go.uber.org/zap/exp v0.3.0 golang.org/x/arch v0.26.0 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f @@ -52,6 +52,9 @@ require ( ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect @@ -67,10 +70,22 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect github.com/aws/smithy-go v1.25.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -82,25 +97,48 @@ require ( github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/providers/confmap v1.0.0 // indirect github.com/knadh/koanf/v2 v2.3.4 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/collector/confmap v1.56.0 // indirect go.opentelemetry.io/collector/consumer v1.56.0 // indirect go.opentelemetry.io/collector/consumer/consumererror v0.150.0 // indirect go.opentelemetry.io/collector/featuregate v1.56.0 // indirect go.opentelemetry.io/collector/internal/componentalias v0.150.0 // indirect go.opentelemetry.io/collector/pipeline v1.56.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect diff --git a/go.sum b/go.sum index 0f515c27c..056142719 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,11 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A= @@ -34,23 +42,51 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcu github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elastic/go-freelru v0.16.0 h1:gG2HJ1WXN2tNl5/p40JS/l59HjvjRhjyAa+oFTRArYs= github.com/elastic/go-freelru v0.16.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I= github.com/elastic/go-perf v0.0.0-20260224073651-af0ee0c731b7 h1:fGi5uudj7m5O1RgQl+bSZmwlqvuUMEi97X7TzWBOMGk= github.com/elastic/go-perf v0.0.0-20260224073651-af0ee0c731b7/go.mod h1:ucTo2u8JvFyIPQOaRlX7aVF0d3wwmF1dy/PVp5GUHZI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -63,11 +99,14 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= @@ -92,6 +131,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d h1:JmrZTpS0GAyMV4ZQVVH/AS0Y6r2PbnYNSRUuRX+HOLA= github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d/go.mod h1:+SexPO1ZvdbbWUdUnyXEWv3+4NwHZjKhxOmQqHY4Pqc= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= @@ -107,25 +150,66 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/open-telemetry/sig-profiling/profcheck v0.0.0-20260427172309-47cc6a9c3929 h1:aCWNMp+ydtLE5h0y2+4iAsYAKLC0tYEabtze3KZJ4j4= github.com/open-telemetry/sig-profiling/profcheck v0.0.0-20260427172309-47cc6a9c3929/go.mod h1:WPdgk1BinVdvYdkbt1KIkgDw6qUiK8CnGq+ftaJ41Ns= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= @@ -168,8 +252,14 @@ go.opentelemetry.io/collector/receiver/receivertest v0.150.0 h1:D34dL/NxP+MTMWZs go.opentelemetry.io/collector/receiver/receivertest v0.150.0/go.mod h1:/MWpPrRvljhZpbSTOHijr69Kg1A/MhUoKX0tLZpkhgE= go.opentelemetry.io/collector/receiver/xreceiver v0.150.0 h1:UpgWq1saq6QWGawJzKpJfLmcv52qBLBRjsv3vcy5fLM= go.opentelemetry.io/collector/receiver/xreceiver v0.150.0/go.mod h1:ltPXHfF5wjxmIti1GfGfAzOeBpovRMePdFj96kefsT0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -201,6 +291,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4= golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= @@ -217,16 +309,28 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= @@ -236,5 +340,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/interpreter/go/go_test.go b/interpreter/go/go_test.go index e6d4cdc94..7744d96d0 100644 --- a/interpreter/go/go_test.go +++ b/interpreter/go/go_test.go @@ -33,7 +33,7 @@ func BenchmarkGolang(b *testing.B) { if err != nil { b.Fatalf("Failed to create hostID: %v", err) } - loaderInfo := interpreter.NewLoaderInfo(hostFileID, elfRef) + loaderInfo := interpreter.NewLoaderInfo(hostFileID, elfRef, nil) rm := remotememory.NewProcessVirtualMemory(libpfPID) b.ReportAllocs() diff --git a/interpreter/instancestubs.go b/interpreter/instancestubs.go index 1eca53dea..00e28279a 100644 --- a/interpreter/instancestubs.go +++ b/interpreter/instancestubs.go @@ -4,11 +4,16 @@ package interpreter // import "go.opentelemetry.io/ebpf-profiler/interpreter" import ( + "unsafe" + + "go.opentelemetry.io/ebpf-profiler/host" "go.opentelemetry.io/ebpf-profiler/libc" "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/lpm" "go.opentelemetry.io/ebpf-profiler/metrics" "go.opentelemetry.io/ebpf-profiler/process" "go.opentelemetry.io/ebpf-profiler/reporter" + "go.opentelemetry.io/ebpf-profiler/util" ) // InstanceStubs provides empty implementations of Instance hooks that are @@ -36,3 +41,32 @@ func (is *InstanceStubs) GetAndResetMetrics() ([]metrics.Metric, error) { func (is *InstanceStubs) ReleaseResources() error { return nil } + +type EbpfHandlerStubs struct{} + +func (m *EbpfHandlerStubs) UpdatePidInterpreterMapping(_ libpf.PID, + pfx lpm.Prefix, _ uint8, _ host.FileID, _ uint64) error { + return nil +} + +func (m *EbpfHandlerStubs) DeletePidInterpreterMapping(_ libpf.PID, _ lpm.Prefix) error { + return nil +} + +func (m *EbpfHandlerStubs) CoredumpTest() bool { + return false +} + +func (m *EbpfHandlerStubs) UpdateProcData(libpf.InterpreterType, libpf.PID, + unsafe.Pointer) error { + return nil +} + +func (m *EbpfHandlerStubs) DeleteProcData(libpf.InterpreterType, libpf.PID) error { + return nil +} + +func (m *EbpfHandlerStubs) UpdateInterpreterOffsets(ebpfProgIndex uint16, fileID host.FileID, + offsetRanges []util.Range) error { + return nil +} diff --git a/interpreter/loaderinfo.go b/interpreter/loaderinfo.go index b7d9513ad..b0fe697df 100644 --- a/interpreter/loaderinfo.go +++ b/interpreter/loaderinfo.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/ebpf-profiler/host" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" "go.opentelemetry.io/ebpf-profiler/util" ) @@ -19,13 +20,17 @@ type LoaderInfo struct { fileID host.FileID // elfRef provides a cached access to the ELF file. elfRef *pfelf.Reference + // deltas contains the stack deltas for the executable. + deltas sdtypes.StackDeltaArray } // NewLoaderInfo returns a populated LoaderInfo struct. -func NewLoaderInfo(fileID host.FileID, elfRef *pfelf.Reference) *LoaderInfo { +func NewLoaderInfo(fileID host.FileID, elfRef *pfelf.Reference, + deltas sdtypes.StackDeltaArray) *LoaderInfo { return &LoaderInfo{ fileID: fileID, elfRef: elfRef, + deltas: deltas, } } @@ -60,3 +65,8 @@ func (i *LoaderInfo) FileID() host.FileID { func (i *LoaderInfo) FileName() string { return i.elfRef.FileName() } + +// Deltas returns the stack deltas for the executable of this LoaderInfo. +func (i *LoaderInfo) Deltas() sdtypes.StackDeltaArray { + return i.deltas +} diff --git a/interpreter/luajit/bc.go b/interpreter/luajit/bc.go new file mode 100644 index 000000000..e572c2399 --- /dev/null +++ b/interpreter/luajit/bc.go @@ -0,0 +1,196 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +// See https://github.com/openresty/luajit2/blob/7952882d/src/lj_bc.h#L34 + +var bcMode = []uint16{ + 0x3183, 0x3183, 0x3983, 0x3983, 0x2183, 0x2183, 0x2503, 0x2503, + 0x2483, 0x2483, 0x2403, 0x2403, 0xb181, 0xb181, 0xb180, 0xb180, + 0xb303, 0xb303, 0xb181, 0xb181, 0x8181, 0x2981, 0x5499, 0x5c99, + 0x6499, 0x6c99, 0x7499, 0x5499, 0x5c99, 0x6499, 0x6c99, 0x7499, + 0x5199, 0x5999, 0x6199, 0x6999, 0x7199, 0x7999, 0x4221, 0xb501, + 0xb701, 0xb381, 0xb481, 0xb401, 0xb102, 0xb281, 0xb185, 0xb505, + 0xb485, 0xb405, 0xb684, 0x1601, 0x1301, 0x1581, 0x0501, 0x0d03, + 0x0199, 0x0519, 0x0319, 0x0199, 0x099b, 0x0d1b, 0x0b1b, 0x0c82, + 0x099b, 0x4b32, 0x4b32, 0x4b02, 0x4b02, 0x4b32, 0x4b32, 0xb332, + 0xb682, 0xb302, 0xb304, 0xb304, 0xb304, 0xb682, 0xb682, 0xb682, + 0xb682, 0xb302, 0xb682, 0xb682, 0xb302, 0xb684, 0xb684, 0xb304, + 0xb684, 0xb004, 0xb004, 0xb304, 0xb004, 0xb004, 0xb304, 0xb004, + 0xb004} + +func bcOp(ins uint32) uint32 { + return ins & 0xff +} + +func bcModeMM(op uint32) uint32 { + return uint32(bcMode[op] >> 11) +} + +func bcModeA(op uint32) uint32 { + return uint32(bcMode[op] & 7) +} + +func bcA(ins uint32) uint32 { + return (ins >> 8) & 0xff +} + +func bcB(ins uint32) uint32 { + return ins >> 24 +} + +func bcC(ins uint32) uint32 { + return (ins >> 16) & 0xff +} + +func bcD(ins uint32) uint32 { + return ins >> 16 +} + +// Return the register used to store the function called at pc or metaname if it's a metamethod +func getSlotOrMetaname(ins uint32) (slot uint32, metaname string) { + op := bcOp(ins) + mm := bcModeMM(op) + if mm == MMcall { + slot := bcA(ins) + if bcOp(ins) == BC_ITERC { + slot -= 3 + } + return slot, "" + } else if mm != MMMax { + return 0, ljMetaNames[mm] + } + return 0, "" +} +func bcModeAIsBase(op uint32) bool { + return bcModeA(op) == BCMbase +} +func bcModeAIsDst(op uint32) bool { + return bcModeA(op) == BCMdst +} + +var ljMetaNames = []string{ + "index", "newindex", "gc", + "mode", "eq", "len", "lt", "le", "concat", + "call", "add", "sub", "mul", "div", "mod", "pow", "unm", + "metatable", "tostring", "new", "pairs", + "ipairs", +} + +const ( + BCMdst = 1 + BCMbase = 2 + MMcall = 9 + MMMax = 0x16 +) + +//nolint:stylecheck +const ( + BC_ISLT = iota + BC_ISGE + BC_ISLE + BC_ISGT + BC_ISEQV + BC_ISNEV + BC_ISEQS + BC_ISNES + BC_ISEQN + BC_ISNEN + BC_ISEQP + BC_ISNEP + BC_ISTC + BC_ISFC + BC_IST + BC_ISF + BC_ISTYPE + BC_ISNUM + BC_MOV + BC_NOT + BC_UNM + BC_LEN + BC_ADDVN + BC_SUBVN + BC_MULVN + BC_DIVVN + BC_MODVN + BC_ADDNV + BC_SUBNV + BC_MULNV + BC_DIVNV + BC_MODNV + BC_ADDVV + BC_SUBVV + BC_MULVV + BC_DIVVV + BC_MODVV + BC_POW + BC_CAT + BC_KSTR + BC_KCDATA + BC_KSHORT + BC_KNUM + BC_KPRI + BC_KNIL + BC_UGET + BC_USETV + BC_USETS + BC_USETN + BC_USETP + BC_UCLO + BC_FNEW + BC_TNEW + BC_TDUP + BC_GGET + BC_GSET + BC_TGETV + BC_TGETS + BC_TGETB + BC_TGETR + BC_TSETV + BC_TSETS + BC_TSETB + BC_TSETM + BC_TSETR + BC_CALLM + BC_CALL + BC_CALLMT + BC_CALLT + BC_ITERC + BC_ITERN + BC_VARG + BC_ISNEXT + BC_RETM + BC_RET + BC_RET0 + BC_RET1 + BC_FORI + BC_JFORI + BC_FORL + BC_IFORL + BC_JFORL + BC_ITERL + BC_IITERL + BC_JITERL + BC_LOOP + BC_ILOOP + BC_JLOOP + BC_JMP + BC_FUNCF + BC_IFUNCF + BC_JFUNCF + BC_FUNCV + BC_IFUNCV + BC_JFUNCV + BC_FUNCC + BC_FUNCCW + BC__MAX +) diff --git a/interpreter/luajit/extractor_aarch64.go b/interpreter/luajit/extractor_aarch64.go new file mode 100644 index 000000000..818734827 --- /dev/null +++ b/interpreter/luajit/extractor_aarch64.go @@ -0,0 +1,359 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import ( + "errors" + "reflect" + + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "golang.org/x/arch/arm64/arm64asm" +) + +type armExtractor struct { + ef *pfelf.File +} + +var _ extractor = &armExtractor{} + +// Return true if the code in b calls targetCall. +func (a *armExtractor) callExists(b []byte, baseAddr, targetCall int64) (bool, error) { + var ip int64 + for ; len(b) > 0; b = b[4:] { + ip += 4 + i, err := arm64asm.Decode(b) + if err != nil { + return false, err + } + if i.Op == arm64asm.BL { + a0, ok := i.Args[0].(arm64asm.PCRel) + if ok { + result := baseAddr + ip + int64(a0) + if result == targetCall { + return true, nil + } + } + } + } + return false, nil +} + +// This function gets the glref offset from the first load and the +// the cur_L offset from the last store instruction. Its not resilient +// to arbitrary register movement/spilling but seems to work. +// +// (lldb) dis -n lua_close +// libluajit-5.1.so`lua_close: +// libluajit-5.1.so[0x15c20] <+0>: stp x19, x20, [sp, #-0x30]! +// libluajit-5.1.so[0x15c24] <+4>: ldr x20, [x0, #0x10] ; 0x10 is glrefOffset +// libluajit-5.1.so[0x15c28] <+8>: stp x21, x22, [sp, #0x10] +// libluajit-5.1.so[0x15c2c] <+12>: adrp x21, 0 +// libluajit-5.1.so[0x15c30] <+16>: mov w22, #0xa ; =10 +// libluajit-5.1.so[0x15c34] <+20>: add x21, x21, #0x7d4 +// libluajit-5.1.so[0x15c38] <+24>: ldr x19, [x20, #0xc0] +// libluajit-5.1.so[0x15c3c] <+28>: str x30, [sp, #0x20] +// libluajit-5.1.so[0x15c40] <+32>: mov x0, x19 +// libluajit-5.1.so[0x15c44] <+36>: bl 0x8040 ; symbol stub for: luaJIT_profile_start +// libluajit-5.1.so[0x15c48] <+40>: ldr x1, [x19, #0x38] +// libluajit-5.1.so[0x15c4c] <+44>: str xzr, [x20, #0x170] ; 0x170 is curLOffset +func (a *armExtractor) findOffsetsFromLuaClose(b []byte) (glref, curL uint64, err error) { + var greg arm64asm.Reg + for ; len(b) > 0; b = b[4:] { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, 0, err + } + // ldr x20, [x0, #0x10] ; 0x10 is glrefOffset + if i.Op == arm64asm.LDR && greg == 0 { + a1, ok := i.Args[1].(arm64asm.MemImmediate) + if ok { + glref = getImm(a1) + greg = i.Args[0].(arm64asm.Reg) + } + } + if i.Op == arm64asm.STR { + a1, ok := i.Args[1].(arm64asm.MemImmediate) + if ok && arm64asm.Reg(a1.Base) == greg && i.Args[0] == arm64asm.XZR { + curL = getImm(a1) + break + } + } + } + return glref, curL, nil +} + +// libluajit-5.1.so[0x145e4] <+4>: mov x19, x0 +// ... +// libluajit-5.1.so[0x14660] <+128>: add x3, x19, #0xf38 +func (a *armExtractor) findG2DispatchOffsetFromLjDispatchUpdate(b []byte) (uint64, error) { + greg := arm64asm.X0 + for ; len(b) > 0; b = b[4:] { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, err + } + // Update greg if it moves + if i.Op == arm64asm.MOV { + a0, ok0 := i.Args[0].(arm64asm.Reg) + a1, ok1 := i.Args[1].(arm64asm.Reg) + if ok0 && ok1 && a1 == arm64asm.X0 { + greg = a0 + } + } + if i.Op == arm64asm.ADD && greg != 0 { + a1, ok := i.Args[1].(arm64asm.RegSP) + if ok && arm64asm.Reg(a1) == greg { + a2, ok := i.Args[2].(arm64asm.ImmShift) + if ok { + return getImmU(a2), nil + } + } + } + } + return 0, errors.New("g to dispatch offset not found") +} + +func (a *armExtractor) findLjDispatchUpdateAddr(b []byte, addr uint64) (uint64, error) { + var ip int64 + for len(b) > 0 { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, err + } + if i.Op == arm64asm.BL { + a0, ok := i.Args[0].(arm64asm.PCRel) + if ok { + offset := int64(a0) + result := int64(addr) + ip + offset + return uint64(result), nil + } + } + ip += 4 + b = b[4:] + } + return 0, errors.New("no calls in code") +} + +// libluajit-5.1.so`lj_cf_jit_util_traceinfo: +// libluajit-5.1.so[0x67a44] <+0>: stp x19, x20, [sp, #-0x40]! +// libluajit-5.1.so[0x67a48] <+4>: mov w1, #0x1 ; =1 +// libluajit-5.1.so[0x67a4c] <+8>: mov x19, x0 +// libluajit-5.1.so[0x67a50] <+12>: str x30, [sp, #0x30] +// libluajit-5.1.so[0x67a54] <+16>: bl 0x5adf0 ; lj_lib_checkint at lj_lib.c:242:1 +// libluajit-5.1.so[0x67a58] <+20>: cbz w0, 0x67be8 ; <+420> at lib_jit.c:381:10 +// libluajit-5.1.so[0x67a5c] <+24>: ldr x2, [x19, #0x10] ;; This loads global +// libluajit-5.1.so[0x67a60] <+28>: mov w1, w0 +// libluajit-5.1.so[0x67a64] <+32>: mov w0, #0x0 ; =0 +// libluajit-5.1.so[0x67a68] <+36>: add x2, x2, #0x2e0 ;; This is global to J offset +// libluajit-5.1.so[0x67a6c] <+40>: ldr w3, [x2, #0x174] ;; This is checking J->sztraces != 0 +// libluajit-5.1.so[0x67a70] <+44>: cmp w1, w3 +// libluajit-5.1.so[0x67a74] <+48>: b.hs 0x67bdc ; <+408> at lib_jit.c:382:1 +// libluajit-5.1.so[0x67a78] <+52>: ldr x2, [x2, #0x168] ;; This is J->trace +// So for this version we want 0x2e0 + 0x168 +func (a *armExtractor) findG2TracesOffsetFromChecktrace(b []byte) (uint64, error) { + var reg arm64asm.Reg + var G2JOffset uint64 + sawSZTraceLoad := false + for len(b) > 0 { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, err + } + if i.Op == arm64asm.LDR { + a1, ok := i.Args[1].(arm64asm.MemImmediate) + if ok { + imm := getImm(a1) + if imm == 0x10 { + reg = i.Args[0].(arm64asm.Reg) + } else if arm64asm.Reg(a1.Base) == reg { + // Skip over load of sztraces + if sawSZTraceLoad { + return G2JOffset + imm, nil + } + sawSZTraceLoad = true + } + } + } + if i.Op == arm64asm.ADD { + a1, ok := i.Args[1].(arm64asm.RegSP) + if ok && arm64asm.Reg(a1) == reg { + a2, ok := i.Args[2].(arm64asm.ImmShift) + if ok { + G2JOffset = getImmU(a2) + } + } + } + b = b[4:] + } + return 0, errors.New("offset not found") +} + +// luaopen_jit looks like this. ___lldb_unnamed_symbol1372 is lj_lib_prereg, the 2nd call to it +// is for luaopen_jit_util, so we want to get the address that is constructed in the x2 register +// and return it. +// +// Source: +// https://github.com/openresty/luajit2/blob/7952882d9/src/lib_jit.c#L803 +// +// libluajit-5.1.so[0x64d88] <+168>: add x2, x20, #0xd0 +// libluajit-5.1.so[0x64d8c] <+172>: add x3, x21, #0xb8 +// libluajit-5.1.so[0x64d90] <+176>: mov x0, x19 +// libluajit-5.1.so[0x64d94] <+180>: adrp x1, 8 +// libluajit-5.1.so[0x64d98] <+184>: add x1, x1, #0xa28 +// libluajit-5.1.so[0x64d9c] <+188>: bl 0x57e50 ; ___lldb_unnamed_symbol1370 +// libluajit-5.1.so[0x64da0] <+192>: ldr x3, [x19, #0x48] +// libluajit-5.1.so[0x64da4] <+196>: mov x0, x19 +// libluajit-5.1.so[0x64da8] <+200>: adrp x2, -1 +// libluajit-5.1.so[0x64dac] <+204>: adrp x1, 8 +// libluajit-5.1.so[0x64db0] <+208>: add x2, x2, #0x338 +// libluajit-5.1.so[0x64db4] <+212>: add x1, x1, #0xa30 +// libluajit-5.1.so[0x64db8] <+216>: bl 0x58320 ; ___lldb_unnamed_symbol1372 +// libluajit-5.1.so[0x64dbc] <+220>: ldr x3, [x19, #0x48] +// libluajit-5.1.so[0x64dc0] <+224>: mov x0, x19 +// libluajit-5.1.so[0x64dc4] <+228>: adrp x2, -1 +// libluajit-5.1.so[0x64dc8] <+232>: adrp x1, 8 +// libluajit-5.1.so[0x64dcc] <+236>: add x2, x2, #0x310 +// libluajit-5.1.so[0x64dd0] <+240>: add x1, x1, #0xa40 +// libluajit-5.1.so[0x64dd4] <+244>: bl 0x58320 ; ___lldb_unnamed_symbol1372 +// libluajit-5.1.so[0x64dd8] <+248>: add x3, x21, #0xf0 +// libluajit-5.1.so[0x64ddc] <+252>: add x2, x20, #0x130 +// libluajit-5.1.so[0x64de0] <+256>: mov x0, x19 +// libluajit-5.1.so[0x64de4] <+260>: adrp x1, 8 +// libluajit-5.1.so[0x64de8] <+264>: add x1, x1, #0xa50 +// libluajit-5.1.so[0x64dec] <+268>: bl 0x57e50 ; ___lldb_unnamed_symbol1370 +// +// So we track adrp and add instructions touching x2 and return that value when we see the +// a repeat BL call. In this case: +// [0x64dc4] <+228>: adrp x2, -1 --> x2 becomes 0x63000 +// [0x64dcc] <+236>: add x2, x2, #0x310 --> x2 becomes 0x63310 +func (a *armExtractor) find3rdArgToLibPreregCall(b []byte, addr int64) (uint64, error) { + var ip, x2, prevCall int64 + for ; len(b) > 0; b = b[4:] { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, err + } + if i.Op == arm64asm.BL { + a0, ok := i.Args[0].(arm64asm.PCRel) + if ok { + result := addr + ip + int64(a0) + // There's also two back to back calls to lua_copy ignore those + // by requiring x2 to have been set. + if result == prevCall && x2 != 0 { + return uint64(x2), nil + } + prevCall = result + } + } + if i.Op == arm64asm.ADRP { + a0, ok1 := i.Args[0].(arm64asm.Reg) + a1, ok2 := i.Args[1].(arm64asm.PCRel) + if ok1 && ok2 && a0 == arm64asm.X2 { + // zero lower 12 bits of addr+ip + x2 = (addr + ip) & ^0xfff + x2 += int64(a1) + } + } + if i.Op == arm64asm.ADD { + a0, ok1 := i.Args[0].(arm64asm.RegSP) + a1, ok2 := i.Args[1].(arm64asm.RegSP) + if ok1 && ok2 && arm64asm.Reg(a1) == arm64asm.X2 && a0 == a1 { + a2, ok := i.Args[2].(arm64asm.ImmShift) + if ok { + x2 += int64(getImmU(a2)) + } + } + } + ip += 4 + } + return 0, errors.New("failed to find 3rd arg to lib prereg call") +} + +// The 4th arg to lj_lib_register is lj_lib_cf_jit_util which is a function array. +// Track the adrp/add combo the x3 register to get it. +// +// Source: +// https://github.com/openresty/luajit2/blob/7952882/src/lib_jit.c#L486 +// +// libluajit-5.1.so`___lldb_unnamed_symbol1577: +// libluajit-5.1.so[0x63310] <+0>: str x30, [sp, #-0x10]! +// libluajit-5.1.so[0x63314] <+4>: mov x1, #0x0 ; =0 +// libluajit-5.1.so[0x63318] <+8>: adrp x3, 43 +// libluajit-5.1.so[0x6331c] <+12>: adrp x2, 9 +// libluajit-5.1.so[0x63320] <+16>: add x3, x3, #0xfc8 +// libluajit-5.1.so[0x63324] <+20>: add x2, x2, #0x740 +// libluajit-5.1.so[0x63328] <+24>: bl 0x57e50 ; ___lldb_unnamed_symbol1370 +// libluajit-5.1.so[0x6332c] <+28>: mov w0, #0x1 ; =1 +// libluajit-5.1.so[0x63330] <+32>: ldr x30, [sp], #0x10 +// libluajit-5.1.so[0x63334] <+36>: ret +func (a *armExtractor) find4thArgToLibRegCall(b []byte, addr int64) (int64, error) { + var ip, x3 int64 + for ; len(b) > 0; b = b[4:] { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, err + } + if i.Op == arm64asm.ADRP { + a0, ok1 := i.Args[0].(arm64asm.Reg) + a1, ok2 := i.Args[1].(arm64asm.PCRel) + if ok1 && ok2 && a0 == arm64asm.X3 { + // zero lower 12 bits of addr+ip + x3 = (addr + ip) & ^0xfff + x3 += int64(a1) + } + } + if i.Op == arm64asm.ADD { + a0, ok1 := i.Args[0].(arm64asm.RegSP) + a1, ok2 := i.Args[1].(arm64asm.RegSP) + if ok1 && ok2 && arm64asm.Reg(a1) == arm64asm.X3 && a0 == a1 { + a2, ok := i.Args[2].(arm64asm.ImmShift) + if ok { + x3 += int64(getImmU(a2)) + return x3, nil + } + } + } + ip += 4 + } + return 0, errors.New("failed to find 4th arg to lj_lib_register call") +} + +func (a *armExtractor) findFirstCall(b []byte, addr int64) (uint64, error) { + var ip int64 + for ; len(b) > 0; b = b[4:] { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, err + } + if i.Op == arm64asm.BL { + a0, ok := i.Args[0].(arm64asm.PCRel) + if ok { + result := addr + ip + int64(a0) + return uint64(result), nil + } + } + ip += 4 + } + return 0, errors.New("no calls found") +} + +func getImm(m any) uint64 { + //https://github.com/golang/go/issues/57684 + imm := reflect.ValueOf(m).FieldByName("imm") + return uint64(imm.Int()) +} + +func getImmU(m any) uint64 { + //https://github.com/golang/go/issues/57684 + imm := reflect.ValueOf(m).FieldByName("imm") + return imm.Uint() +} diff --git a/interpreter/luajit/extractor_x86.go b/interpreter/luajit/extractor_x86.go new file mode 100644 index 000000000..b8f846096 --- /dev/null +++ b/interpreter/luajit/extractor_x86.go @@ -0,0 +1,520 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import ( + "errors" + + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + xh "go.opentelemetry.io/ebpf-profiler/x86helpers" + "golang.org/x/arch/x86/x86asm" +) + +type x86Extractor struct { + ef *pfelf.File +} + +var _ extractor = &x86Extractor{} + +/* +* +Dump of assembler code for function lua_close: + +Get the offset global_State pointer in lua_State (glref) and the offset +of the lua_State pointer in global_State (cur_L) from the disassembly of lua_close +which is a dynamic public symbol that should be in all binaries of LuaJIT including stripped. + + 0x0000000000016d80 <+0>: push %r13 + 0x0000000000016d82 <+2>: push %r12 + 0x0000000000016d84 <+4>: lea -0x33b(%rip),%r12 # 0x16a50 + 0x0000000000016d8b <+11>: push %rbp + 0x0000000000016d8c <+12>: push %rbx + 0x0000000000016d8d <+13>: mov $0xa,%r13d + 0x0000000000016d93 <+19>: sub $0x8,%rsp + 0x0000000000016d97 <+23>: mov 0x10(%rdi),%rbp ; 0x10 is the glrefOffset + 0x0000000000016d9b <+27>: mov 0xc0(%rbp),%rbx + 0x0000000000016da2 <+34>: mov %rbx,%rdi + 0x0000000000016da5 <+37>: call 0x1f6f0 + 0x0000000000016daa <+42>: mov 0x38(%rbx),%rsi + 0x0000000000016dae <+46>: mov %rbx,%rdi + 0x0000000000016db1 <+49>: movq $0x0,0x170(%rbp) ; 0x170 is curLOffset +*/ +//nolint:nonamedreturns +func (x *x86Extractor) findOffsetsFromLuaClose(b []byte) (glref, curL uint64, err error) { + b, _ = xh.SkipEndBranch(b) //nolint:errcheck + var greg x86asm.Reg + var zeroReg x86asm.Reg + for len(b) > 0 { + var i x86asm.Inst + i, err = x86asm.Decode(b, 64) + if err != nil { + return 0, 0, err + } + if i.Op == x86asm.XOR { + a0, ok1 := i.Args[0].(x86asm.Reg) + a1, ok2 := i.Args[1].(x86asm.Reg) + if ok1 && ok2 && a0 == a1 { + zeroReg = a0 + } + } + if i.Op == x86asm.MOV { + if greg == 0 { + a0, ok1 := i.Args[0].(x86asm.Reg) + a1, ok2 := i.Args[1].(x86asm.Mem) + if ok1 && ok2 && a1.Base == x86asm.RDI { + greg = a0 + glref = uint64(a1.Disp) + } + } else { + a0, ok1 := i.Args[0].(x86asm.Mem) + if ok1 && sameReg(a0.Base, greg) { + imm, ok2 := i.Args[1].(x86asm.Imm) + if ok2 && imm == 0 { + curL = uint64(a0.Disp) + return glref, curL, nil + } + r1, ok2 := i.Args[1].(x86asm.Reg) + if ok2 && sameReg(r1, zeroReg) { + curL = uint64(a0.Disp) + return glref, curL, nil + } + } + // If Greg is dest error + if r0, ok := i.Args[0].(x86asm.Reg); ok && sameReg(r0, greg) { + err = errors.New("parse error, register holding G was clobbered") + return 0, 0, err + } + } + } + b = b[i.Len:] + } + return 0, 0, errors.New("find offsets from lua_close failed") +} + +// This is different in most builds and we need to get it from stripped binaries. +// The public symbol luaopen_jit gives is the best way in. The first or second +// thing it calls is lj_dispatch_update. We can determine which because the first +// arg is G which will come from the glref offset from L. Ie: +// +// 0x000000000006a737 <+119>: mov 0x10(%rbx),%rdi +// 0x000000000006a73b <+123>: call 0x16cf0 +// +// Then we load the function 0x16cf0 and look where the rdi register is stashed, +// usually rdx and then look for the first lea of rdx, ie: +// +// libluajit-5.1.so[0x16d4e] <+94>: leaq 0xfa8(%rdx), %r10 +// +// 0xfa8 is the g to dispatch offset. +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_dispatch.c#L122 +func (x *x86Extractor) findG2DispatchOffsetFromLjDispatchUpdate(b []byte) (uint64, error) { + b, _ = xh.SkipEndBranch(b) //nolint:errcheck + var greg x86asm.Reg + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + // Early on we stash rdi (g) in a register + if i.Op == x86asm.MOV { + a0, ok1 := i.Args[0].(x86asm.Reg) + a1, ok2 := i.Args[1].(x86asm.Reg) + if ok1 && ok2 && a1 == x86asm.RDI { + greg = a0 + } + } + // Then load dispatch address relative to g + if i.Op == x86asm.LEA { + a1, ok := i.Args[1].(x86asm.Mem) + if ok && a1.Base == greg { + return uint64(a1.Disp), nil + } + } + b = b[i.Len:] + } + return 0, nil +} + +// Find first or second call address, the one whose first argument is 0x10 off of +// sym's first argument. +// libluajit-5.1.so`luaopen_jit: +// libluajit-5.1.so[0x64dd0] <+0>: pushq %rbp +// libluajit-5.1.so[0x64dd1] <+1>: pushq %rbx +// libluajit-5.1.so[0x64dd2] <+2>: movq %rdi, %rbx +// libluajit-5.1.so[0x64dd5] <+5>: xorl %edi, %edi +// libluajit-5.1.so[0x64dd7] <+7>: subq $0x38, %rsp +// libluajit-5.1.so[0x64ddb] <+11>: movq %rsp, %rsi +// libluajit-5.1.so[0x64dde] <+14>: callq 0xd3b6 ; lj_vm_cpuid +// libluajit-5.1.so[0x64de3] <+19>: testl %eax, %eax +// libluajit-5.1.so[0x64de5] <+21>: jne 0x64f18 ; <+328> [inlined] jit_cpudetect at lib_jit.c:677:33 +// libluajit-5.1.so[0x64deb] <+27>: movl $0x3ff0001, %eax ; imm = 0x3FF0001 +// libluajit-5.1.so[0x64df0] <+32>: movq 0x10(%rbx), %rdx +// libluajit-5.1.so[0x64df4] <+36>: movdqa 0x7e24(%rip), %xmm0 ; jit_param_default +// libluajit-5.1.so[0x64dfc] <+44>: movdqa 0x7e2c(%rip), %xmm1 ; jit_param_default + 16 +// libluajit-5.1.so[0x64e04] <+52>: movdqa 0x7e34(%rip), %xmm2 ; jit_param_default + 32 +// libluajit-5.1.so[0x64e0c] <+60>: movups %xmm0, 0x910(%rdx) +// libluajit-5.1.so[0x64e13] <+67>: movups %xmm1, 0x920(%rdx) +// libluajit-5.1.so[0x64e1a] <+74>: movups %xmm2, 0x930(%rdx) +// libluajit-5.1.so[0x64e21] <+81>: movl %eax, 0x350(%rdx) +// libluajit-5.1.so[0x64e27] <+87>: leaq 0x910(%rdx), %rax +// libluajit-5.1.so[0x64e2e] <+94>: movq 0x7e1b(%rip), %rdx ; jit_param_default + 48 +// libluajit-5.1.so[0x64e35] <+101>: movq %rdx, 0x30(%rax) +// libluajit-5.1.so[0x64e39] <+105>: movl 0x7e19(%rip), %edx ; jit_param_default + 56 +// libluajit-5.1.so[0x64e3f] <+111>: movl %edx, 0x38(%rax) +// libluajit-5.1.so[0x64e42] <+114>: movq 0x10(%rbx), %rdi +// libluajit-5.1.so[0x64e46] <+118>: callq 0x15c90 ; lj_dispatch_update at lj_dispatch.c:109:53 +// +//nolint:lll +func (x *x86Extractor) findLjDispatchUpdateAddr(b []byte, addr uint64) (uint64, error) { + b, ip := xh.SkipEndBranch(b) + var Lreg x86asm.Reg + rdiHasG := false + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + if i.Op == x86asm.MOV { + if a1, ok1 := i.Args[1].(x86asm.Reg); ok1 && a1 == x86asm.RDI { + if a0, ok0 := i.Args[0].(x86asm.Reg); ok0 { + Lreg = a0 + } + } + if a0, ok := i.Args[0].(x86asm.Reg); ok && a0 == x86asm.RDI { + if a1, ok1 := i.Args[1].(x86asm.Mem); ok1 { + // Look for: movq 0x10(%rbx), %rdi + if a1.Base == Lreg && a1.Disp == 0x10 { + rdiHasG = true + } + } + } + } + + if i.Op == x86asm.CALL && rdiHasG { + offset := int64(i.Args[0].(x86asm.Rel)) + callAddr := int64(addr) + ip + offset + int64(i.Len) + // TODO: make sure callAddr is within .text bounds? + if callAddr < 0 { + return 0, errors.New("invalid call address") + } + return uint64(callAddr), nil + } + ip += int64(i.Len) + b = b[i.Len:] + } + return 0, errors.New("lj_dispatch_update addr not found") +} + +// libluajit-5.1.so`jit_checktrace: +// libluajit-5.1.so[0x63780] <+0>: pushq %rbx +// libluajit-5.1.so[0x63781] <+1>: movl $0x1, %esi +// libluajit-5.1.so[0x63786] <+6>: movq %rdi, %rbx +// libluajit-5.1.so[0x63789] <+9>: callq 0x58550 ; lj_lib_checkint at lj_lib.c:239:1 +// libluajit-5.1.so[0x6378e] <+14>: xorl %r8d, %r8d +// libluajit-5.1.so[0x63791] <+17>: testl %eax, %eax +// libluajit-5.1.so[0x63793] <+19>: je 0x637ae ; <+46> at lib_jit.c:304:1 +// libluajit-5.1.so[0x63795] <+21>: movq 0x10(%rbx), %rdx +// libluajit-5.1.so[0x63799] <+25>: cmpl %eax, 0x43c(%rdx) +// libluajit-5.1.so[0x6379f] <+31>: jbe 0x637ae ; <+46> at lib_jit.c:304:1 +// ----------- 0x430 is the G to J->traces offset +// libluajit-5.1.so[0x637a1] <+33>: movq 0x430(%rdx), %rdx +func (x *x86Extractor) findG2TracesOffsetFromChecktrace(b []byte) (uint64, error) { + b, _ = xh.SkipEndBranch(b) //nolint:errcheck + var Greg x86asm.Reg + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + if i.Op == x86asm.MOV { + a1, ok := i.Args[1].(x86asm.Mem) + if ok { + // glref offset is 0x10 + if a1.Disp == 0x10 { + Greg = i.Args[0].(x86asm.Reg) + } else if a1.Base == Greg { + return uint64(a1.Disp), nil + } + } + } + b = b[i.Len:] + } + return 0, errors.New("offset not found") +} + +func (x *x86Extractor) findFirstCall(b []byte, baseAddr int64) (uint64, error) { + b, ip := xh.SkipEndBranch(b) + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + if i.Op == x86asm.CALL { + a0, ok := i.Args[0].(x86asm.Rel) + if ok { + // RIP relative calls are relative to next instruction. + callAddr := baseAddr + ip + int64(i.Len) + int64(a0) + return uint64(callAddr), nil + } + } + ip += int64(i.Len) + b = b[i.Len:] + } + return 0, errors.New("no call found") +} + +// Return true if the code in b calls targetCall. +func (x *x86Extractor) callExists(b []byte, baseAddr, targetCall int64) (bool, error) { + b, ip := xh.SkipEndBranch(b) + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return false, err + } + if i.Op == x86asm.CALL { + a0, ok := i.Args[0].(x86asm.Rel) + if ok { + // RIP relative calls are relative to next instruction. + callAddr := baseAddr + ip + int64(i.Len) + int64(a0) + if callAddr == targetCall { + return true, nil + } + } + } + ip += int64(i.Len) + b = b[i.Len:] + } + return false, nil +} + +// luaopen_jit_util will have two of these when lj_lib_prereg is inlined. +// Return the address stored in rsi for the 2nd one. +// 15457: 48 8d 35 12 67 ff ff lea -0x98ee(%rip),%rsi # bb70 +// 1545e: e8 cd 1e 09 00 call a7330 +// +//nolint:lll +func findRipRelativeLea2ndArgTo2ndCall(b []byte, baseAddr, targetCall int64) (uint64, error) { + var leaRsi int64 + calls := 2 + b, ip := xh.SkipEndBranch(b) + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + if i.Op == x86asm.LEA { + a0, ok1 := i.Args[0].(x86asm.Reg) + a1, ok2 := i.Args[1].(x86asm.Mem) + if ok1 && ok2 { + if a0 == x86asm.RSI && a1.Base == x86asm.RIP { + leaRsi = calcRipRelativeAddr(a1, baseAddr, ip+int64(i.Len)) + } + } + } + if i.Op == x86asm.CALL { + a0, ok := i.Args[0].(x86asm.Rel) + if ok { + callAddr := baseAddr + ip + int64(i.Len) + int64(a0) + if callAddr == targetCall { + calls-- + if calls == 0 { + return uint64(leaRsi), nil + } + } + } + } + ip += int64(i.Len) + b = b[i.Len:] + } + return 0, errors.New("failed to find rip relative lea instruction stored in rsi") +} + +//nolint:gocritic +func skipCallsAABA(b []byte, ip, baseAddr int64) ([]byte, int64, error) { + var lastCall int64 + var acall int64 + // 3 Step process, 1 is find AA, 2 is find B and 3 is find A. + step := 0 + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return nil, 0, err + } + if i.Op == x86asm.CALL { + a0, ok := i.Args[0].(x86asm.Rel) + if ok { + callAddr := baseAddr + ip + int64(i.Len) + int64(a0) + if step == 0 && callAddr == lastCall { + // Found potential AA + step = 1 + acall = callAddr + } else if step == 1 && callAddr != lastCall { + // Found AAB + step = 2 + } else if step == 2 && callAddr == acall { + // Found AABA + step = 3 + } else { + // Found different pattern, reset + step = 0 + acall = 0 + } + lastCall = callAddr + } + } + ip += int64(i.Len) + b = b[i.Len:] + if step == 3 { + return b, ip, nil + } + } + return nil, 0, errors.New("failed to find AABA call pattern") +} + +// This function finds the IP relative value passed to lj_lib_prereg as arg 3 (rdx). +// There are 4 of these, we want the 3rd one. +// lj_lib_prereg(L, LUA_JITLIBNAME ".util", luaopen_jit_util, tabref(L->env)); +// 6d965: 48 8b 4b 48 mov 0x48(%rbx),%rcx +// 6d969: 48 89 df mov %rbx,%rdi +// 6d96c: 48 8d 15 ed e2 ff ff lea -0x1d13(%rip),%rdx # 6bc60 +// 6d973: 48 8d 35 1c a2 00 00 lea 0xa21c(%rip),%rsi # 77b96 +// 6d97a: e8 a1 28 ff ff call 60220 +func (x *x86Extractor) find3rdArgToLibPreregCall(b []byte, baseAddr int64) (uint64, error) { + var rdxAddr int64 + calls := 3 + b, ip := xh.SkipEndBranch(b) + // Skip the lua_push* call sequence (and all the preceding calls which varies depending on + // inlining). + // libluajit-5.1.so[0x700a5] <+133>: movq %rbx, %rdi + // libluajit-5.1.so[0x700a8] <+136>: movl $0x5, %edx + // libluajit-5.1.so[0x700ad] <+141>: leaq 0x9b35(%rip), %rsi + // libluajit-5.1.so[0x700b4] <+148>: callq 0x9af0 ; symbol stub for: lua_pushlstring + // libluajit-5.1.so[0x700b9] <+153>: movl $0x3, %edx + // libluajit-5.1.so[0x700be] <+158>: movq %rbx, %rdi + // libluajit-5.1.so[0x700c1] <+161>: leaq 0x9b27(%rip), %rsi + // libluajit-5.1.so[0x700c8] <+168>: callq 0x9af0 ; symbol stub for: lua_pushlstring + // libluajit-5.1.so[0x700cd] <+173>: movq %rbx, %rdi + // libluajit-5.1.so[0x700d0] <+176>: movl $0x4ee7, %esi ; imm = 0x4EE7 + // libluajit-5.1.so[0x700d5] <+181>: callq 0x9360 ; symbol stub for: lua_pushinteger + // libluajit-5.1.so[0x700da] <+186>: movq %rbx, %rdi + // libluajit-5.1.so[0x700dd] <+189>: movl $0x12, %edx + // libluajit-5.1.so[0x700e2] <+194>: leaq 0x9b0a(%rip), %rsi + // libluajit-5.1.so[0x700e9] <+201>: callq 0x9af0 ; symbol stub for: lua_pushlstring + var err error + b, ip, err = skipCallsAABA(b, ip, baseAddr) + if err != nil { + return 0, err + } + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + // Some compilers will use MOV instead of LEA + if i.Op == x86asm.MOV { + a0, ok1 := i.Args[0].(x86asm.Reg) + if ok1 && a0 == x86asm.EDX { + rdxAddr = int64(i.Args[1].(x86asm.Imm)) + } + } + if i.Op == x86asm.LEA { + a0, ok1 := i.Args[0].(x86asm.Reg) + a1, ok2 := i.Args[1].(x86asm.Mem) + if ok1 && ok2 { + if a0 == x86asm.RDX && a1.Base == x86asm.RIP { + rdxAddr = calcRipRelativeAddr(a1, baseAddr, ip+int64(i.Len)) + } + } + } + if i.Op == x86asm.CALL { + calls-- + if calls == 0 { + return uint64(rdxAddr), nil + } + } + ip += int64(i.Len) + b = b[i.Len:] + } + return 0, errors.New("failed to find 3rd arg to lj_lib_prereg call") +} + +// The 4th arg is pointer to function array lj_lib_cf_jit_util +// https://github.com/openresty/luajit2/blob/7952882d/src/lib_jit.c#L486 +// bba0: 48 83 ec 08 sub $0x8,%rsp +// bba4: 48 8d 0d 55 66 0c 00 lea 0xc6655(%rip),%rcx # d2200 <_fini@@Base+0x195aa> +// bbab: 48 8d 15 4e dc 0a 00 lea 0xadc4e(%rip),%rdx # b9800 <_fini@@Base+0xbaa> +// bbb2: 31 f6 xor %esi,%esi +// bbb4: e8 47 55 02 00 call 31100 +// bbb9: b8 01 00 00 00 mov $0x1,%eax +// bbbe: 48 83 c4 08 add $0x8,%rsp +// bbc2: c3 ret +func (x *x86Extractor) find4thArgToLibRegCall(b []byte, baseAddr int64) (int64, error) { + var ip int64 + b, ip = xh.SkipEndBranch(b) + for len(b) > 0 { + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + if i.Op == x86asm.LEA { + a0, ok1 := i.Args[0].(x86asm.Reg) + a1, ok2 := i.Args[1].(x86asm.Mem) + if ok1 && ok2 { + // RCX is 4th arg + if a0 == x86asm.RCX && a1.Base == x86asm.RIP { + return calcRipRelativeAddr(a1, baseAddr, ip+int64(i.Len)), nil + } + } + } + if i.Op == x86asm.MOV { + a0, ok1 := i.Args[0].(x86asm.Reg) + a1, ok2 := i.Args[1].(x86asm.Imm) + if ok1 && ok2 && a0 == x86asm.ECX { + return int64(a1), nil + } + } + ip += int64(i.Len) + b = b[i.Len:] + } + return 0, errors.New("failed to find 4th arg to lj_reg call") +} + +func calcRipRelativeAddr(a1 x86asm.Mem, baseAddr, ip int64) int64 { + // Disp is an int64 but its not set properly and negative numbers + // are 32 bit. TODO: This is a bug that should be created/looked up. + disp := int32(a1.Disp) + return baseAddr + ip + int64(disp) +} + +// If we're dealing with 32bit values compilers will use R or E prefix +// interchangeably (E refs are just zero padded). +func sameReg(r1, r2 x86asm.Reg) bool { + if r1 == r2 { + return true + } + f := func(r1, r2 x86asm.Reg) bool { + switch r1 { + case x86asm.EAX: + return r2 == x86asm.RAX + case x86asm.ECX: + return r2 == x86asm.RCX + case x86asm.EDX: + return r2 == x86asm.RDX + case x86asm.EBX: + return r2 == x86asm.RBX + case x86asm.ESI: + return r2 == x86asm.RSI + default: + return false + } + } + return f(r1, r2) || f(r2, r1) +} diff --git a/interpreter/luajit/log.go b/interpreter/luajit/log.go new file mode 100644 index 000000000..6f04a33b7 --- /dev/null +++ b/interpreter/luajit/log.go @@ -0,0 +1,36 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import ( + "os" + + "go.opentelemetry.io/ebpf-profiler/internal/log" +) + +// Set this to true when LUA_DEBUG env var is set. +var development bool + +func init() { + _, dbgEnv := os.LookupEnv("LUA_DEBUG") + development = dbgEnv +} + +// logf logs luajit debugging as higher level so they stick out w/o +// enabling debug firehose if LUA_DEBUG env var is set. +func logf(format string, args ...interface{}) { + if development { + log.Infof(format, args...) + } else { + log.Debugf(format, args...) + } +} diff --git a/interpreter/luajit/luajit.go b/interpreter/luajit/luajit.go new file mode 100644 index 000000000..3acb1a251 --- /dev/null +++ b/interpreter/luajit/luajit.go @@ -0,0 +1,495 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import ( + "errors" + "fmt" + "path" + "strings" + "sync" + "unsafe" + + "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/internal/log" + "go.opentelemetry.io/ebpf-profiler/interpreter" + "go.opentelemetry.io/ebpf-profiler/libc" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/lpm" + "go.opentelemetry.io/ebpf-profiler/metrics" + sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/process" + "go.opentelemetry.io/ebpf-profiler/remotememory" + "go.opentelemetry.io/ebpf-profiler/reporter" + "go.opentelemetry.io/ebpf-profiler/support" + "go.opentelemetry.io/ebpf-profiler/util" +) + +// Records all the "global" pointers we've seen. +type vmMap map[libpf.Address]struct{} + +// Records all the JIT regions we've seen, value is SynchronizeMappings +// generation. +type regionMap map[process.Mapping]int + +type regionKey struct { + start, end uint64 +} + +type luajitData struct { + // The distance from the "g" pointer in the GG_State struct to the start of the dispatch table. + g2Dispatch uint16 + // The distance from the "g" pointer in the GG_State struct to the start of the trace array + // in the jit_State struct. + g2Traces uint16 + // Offset of cur_L field in the global_State struct. + currentLOffset uint16 +} + +type luajitInstance struct { + rm remotememory.RemoteMemory + protos map[libpf.Address]*proto + jitRegions regionMap + pid libpf.PID + ebpf interpreter.EbpfHandler + // Map of g's we've seen, populated by the symbolizer goroutine and + // consumed in SynchronizeMappings so needs to be protected by a mutex. + mu sync.Mutex + vms vmMap + + // Currently mapped prefixes for each vms traces + prefixesByG map[libpf.Address][]lpm.Prefix + + // Currently mapped prefixes for entire memory regions + prefixes map[regionKey][]lpm.Prefix + + // Hash of the traces for each vm + traceHashes map[libpf.Address]uint64 + cycle int + + g2Traces uint16 +} + +var ( + _ interpreter.Data = &luajitData{} + _ interpreter.Instance = &luajitInstance{} +) + +func (d *luajitData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Address, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + cdata := support.LuaJITProcInfo{ + G2dispatch: d.g2Dispatch, + Cur_L_offset: d.currentLOffset, + Cframe_size_jit: uint16(cframeSizeJIT), + } + if err := ebpf.UpdateProcData(libpf.LuaJIT, pid, unsafe.Pointer(&cdata)); err != nil { + return nil, err + } + + return &luajitInstance{rm: rm, + pid: pid, + ebpf: ebpf, + protos: make(map[libpf.Address]*proto), + jitRegions: make(regionMap), + prefixes: make(map[regionKey][]lpm.Prefix), + prefixesByG: make(map[libpf.Address][]lpm.Prefix), + vms: make(vmMap), + traceHashes: make(map[libpf.Address]uint64), + g2Traces: d.g2Traces, + }, nil +} + +func (d *luajitData) Unload(_ interpreter.EbpfHandler) {} + +func (l *luajitInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { + // Clear memory ranges + for _, prefixes := range l.prefixes { + for _, prefix := range prefixes { + _ = ebpf.DeletePidInterpreterMapping(pid, prefix) + } + } + // Clear trace ranges + for _, prefixes := range l.prefixesByG { + for _, prefix := range prefixes { + _ = ebpf.DeletePidInterpreterMapping(pid, prefix) + } + } + return ebpf.DeleteProcData(libpf.LuaJIT, pid) +} + +func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + base := path.Base(info.FileName()) + if !strings.HasPrefix(base, "libluajit-5.1.so") && + !strings.HasPrefix(base, "luajit") && + base != "nginx" && base != "openresty" { + return nil, nil + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + luaInterp, err := extractInterpreterBounds(info.Deltas(), cframeSize) + if err != nil { + return nil, err + } + logf("lj: interp range %v", luaInterp) + + ljd := &luajitData{} + + if err = extractOffsets(ef, ljd, luaInterp); err != nil { + return nil, err + } + + logf("lj: offsets %+v", ljd) + + if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindLuaJIT, info.FileID(), + []util.Range{luaInterp}); err != nil { + return nil, err + } + + return ljd, nil +} + +// LuaJIT's interpreter isn't a function, its a raw chunk of assembly code with direct threaded +// jumps at end of each opcode. The public entrypoints (lua_pcall/lua_resume) call the lj_vm_pcall +// function at the end of this blob which set up the interpreter and starts executing. +// Even though its not a normal function an eh_frame entry is created for it, its really +// big and has a somewhat unique FDE we can pick out. We could tighten this up by looking for +// direct jumps to the start of the interpreter (one can be found lj_dispatch_update) but we'd +// still need to consult the stack deltas to get the end of the interpreter. +func extractInterpreterBounds(deltas sdtypes.StackDeltaArray, param int32) (util.Range, + error) { + for i := 0; i < len(deltas)-1; i++ { + d, next := &deltas[i], &deltas[i+1] + if next.Address-d.Address > 10_000 { + // The first case covers x86 w/ dwarf and old versions of luajit ARM that used dwarf and + // the second covers more recent arm versions that use frame pointers. + if d.Info.BaseReg == support.UnwindRegSp && d.Info.Param == param || + d.Info.BaseReg == support.UnwindRegFp && d.Info.Param == 16 { + return util.Range{Start: d.Address, End: next.Address}, nil + } + } + } + + return util.Range{}, errors.New("failed to find interpreter range") +} + +func (l *luajitInstance) getVMList() []libpf.Address { + l.mu.Lock() + defer l.mu.Unlock() + gs := make([]libpf.Address, 0, len(l.vms)) + for g := range l.vms { + gs = append(gs, g) + } + return gs +} + +func (l *luajitInstance) addJITRegion(ebpf interpreter.EbpfHandler, pid libpf.PID, + start, end uint64) error { + prefixes, err := lpm.CalculatePrefixList(start, end) + if err != nil { + logf("lj: failed to calculate lpm: %v", err) + return err + } + logf("lj: add JIT region pid(%v) %#x:%#x", pid, start, end) + for _, prefix := range prefixes { + // TODO: fix these: WARN[0267] Failed to lookup file ID 0x2a00000000 + fileID := support.LJFileId << 32 + if err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindLuaJIT, + host.FileID(fileID), 0); err != nil { + return err + } + } + k := regionKey{start: start, end: end} + l.prefixes[k] = prefixes + return nil +} + +func (l *luajitInstance) addTrace(ebpf interpreter.EbpfHandler, pid libpf.PID, t trace, g, + spadjust uint64) ([]lpm.Prefix, error) { + start, end := t.mcode, t.mcode+uint64(t.szmcode) + prefixes, err := lpm.CalculatePrefixList(start, end) + if err != nil { + logf("lj: failed to calculate lpm: %v", err) + return nil, err + } + logf("lj: add trace mapping for pid(%v) %x:%x", pid, start, end) + for _, prefix := range prefixes { + fileID := support.LJFileId<<32 | spadjust + if err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindLuaJIT, + host.FileID(fileID), g); err != nil { + return nil, err + } + } + return prefixes, nil +} + +func (l *luajitInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, + _ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping) error { + return l.synchronizeMappings(ebpf, pr.PID(), mappings) +} + +func (l *luajitInstance) synchronizeMappings(ebpf interpreter.EbpfHandler, pid libpf.PID, + mappings []process.Mapping) error { + cycle := l.cycle + l.cycle++ + for i := range mappings { + m := &mappings[i] + if !m.IsAnonymous() || !m.IsExecutable() { + continue + } + l.jitRegions[*m] = cycle + } + + // Remove old ones + for m, c := range l.jitRegions { + k := regionKey{start: m.Vaddr, end: m.Vaddr + m.Length} + if c != cycle { + for _, prefix := range l.prefixes[k] { + if err := ebpf.DeletePidInterpreterMapping(pid, prefix); err != nil { + return errors.Join(err, fmt.Errorf("failed to delete prefix %v", prefix)) + } + } + delete(l.jitRegions, m) + delete(l.prefixes, k) + } + } + + // Add new ones + for m := range l.jitRegions { + k := regionKey{start: m.Vaddr, end: m.Vaddr + m.Length} + if _, ok := l.prefixes[k]; !ok { + if err := l.addJITRegion(ebpf, pid, m.Vaddr, m.Vaddr+m.Length); err != nil { + return errors.Join(err, fmt.Errorf("failed to add JIT region %v", m)) + } + } + } + + return l.processVMs(ebpf, pid) +} + +func (l *luajitInstance) processVMs(ebpf interpreter.EbpfHandler, pid libpf.PID) error { + var badVMs []libpf.Address + for _, g := range l.getVMList() { + hash, traces, err := loadTraces(g+libpf.Address(l.g2Traces), l.rm) + if err != nil { + // if g is bad remove it + log.Warnf("LuaJIT instance (%v) deleted: %v", g, err) + badVMs = append(badVMs, g) + continue + } + // Don't do anything if nothing changed. + if hash == l.traceHashes[g] { + continue + } + + // We don't bother trying to keep things in sync, just delete them all and re-add them. + prefixes := l.prefixesByG[g] + l.prefixesByG[g] = nil + for _, prefix := range prefixes { + _ = ebpf.DeletePidInterpreterMapping(pid, prefix) + } + + newPrefixes := []lpm.Prefix{} + traceLoop: + for i := range traces { + t := traces[i] + // Validate the trace + foundRegion := false + for reg := range l.jitRegions { + if t.mcode >= reg.Vaddr && t.mcode < reg.Vaddr+reg.Length { + foundRegion = true + end := t.mcode + uint64(t.szmcode) + if end > reg.Vaddr+reg.Length { + log.Errorf("trace %v end goes beyond JIT region, bad szmcode", t) + continue traceLoop + } + break + } + } + + if !foundRegion { + log.Errorf("trace %v not in a JIT region", t) + continue + } + + stackDelta := uint64(t.spadjust) + uint64(cframeSizeJIT) + // If this is a side trace, we need to add the spadjust of the root trace but + // only if they are different. + //https://github.com/openresty/luajit2/blob/7952882d/src/lj_gdbjit.c#L597 + if t.root != 0 && traces[t.root].spadjust != t.spadjust { + stackDelta += uint64(traces[t.root].spadjust) + uint64(cframeSizeJIT) + } + p, err := l.addTrace(ebpf, pid, t, uint64(g), stackDelta) + if err != nil { + log.Errorf("Error adding trace(%d): %v", t.traceno, err) + continue + } + newPrefixes = append(newPrefixes, p...) + } + + log.Infof("LuaJIT traces for pid(%v) added: %d with %d prefixes and removed %d prefixes", + pid, len(traces), len(newPrefixes), len(prefixes)) + + l.prefixesByG[g] = newPrefixes + l.traceHashes[g] = hash + } + l.removeVMs(badVMs) + return nil +} + +func (l *luajitInstance) removeVMs(gs []libpf.Address) { + l.mu.Lock() + defer l.mu.Unlock() + for _, g := range gs { + delete(l.vms, g) + } +} + +func (l *luajitInstance) getGCproto(pt libpf.Address) (*proto, error) { + if pt == 0 { + return nil, nil + } + if gc, ok := l.protos[pt]; ok { + return gc, nil + } + gc, err := newProto(l.rm, pt) + if err != nil { + return nil, err + } + l.protos[pt] = gc + return gc, nil +} + +// symbolizeFrame symbolizes the previous (up the stack) +func (l *luajitInstance) symbolizeFrame(funcName string, ptAddr libpf.Address, + pc uint32, frames *libpf.Frames) error { + pt, err := l.getGCproto(ptAddr) + if err != nil { + return err + } + line := pt.getLine(pc) + fileName := pt.getName() + logf("lj: [%x] %v+%v at %v:%v", ptAddr, funcName, pc, fileName, line) + frames.Append(&libpf.Frame{ + Type: libpf.LuaJITFrame, + FunctionOffset: pc, + FunctionName: libpf.Intern(funcName), + SourceFile: libpf.Intern(fileName), + SourceLine: libpf.SourceLineno(line), + }) + return nil +} + +func (l *luajitInstance) addVM(g libpf.Address) bool { + l.mu.Lock() + defer l.mu.Unlock() + _, ok := l.vms[g] + if !ok { + l.vms[g] = struct{}{} + } + return !ok +} + +func (l *luajitInstance) Symbolize(frame libpf.EbpfFrame, frames *libpf.Frames, fm libpf.FrameMapping) error { + if !frame.Type().IsInterpType(libpf.LuaJIT) { + return interpreter.ErrMismatchInterpreterType + } + + var funcName string + ljkind := frame.Data() + switch ljkind { + case support.LJNormalFrame: + if frame.NumVariables() < 3 { + return errors.New("LuaJIT normal frame not large enough") + } + callerPT := libpf.Address(frame.Variable(1)) + + pt, err := l.getGCproto(callerPT) + if err != nil { + return err + } + + var2 := frame.Variable(2) + callerPC := uint32(var2 & 0xFFFFFFFF) + calleePC := uint32(var2 >> 32) + funcName = pt.getFunctionName(callerPC) + calleePT := libpf.Address(frame.Variable(0)) + if err := l.symbolizeFrame(funcName, calleePT, + calleePC, frames); err != nil { + return err + } + + return nil + case support.LJFFIFunc: + if frame.NumVariables() < 1 { + return errors.New("LuaJIT FFI frame not large enough") + } + funcId := libpf.Address(frame.Variable(0)) & 7 + switch funcId { + case 0: + funcName = "lua-frame" + case 1: + funcName = "c-frame" + case 2: + funcName = "cont-frame" + case 3: + return errors.New("unexpected frame type 3") + case 4: + funcName = "lua-pframe" + case 5: + funcName = "cpcall" + case 6: + funcName = "ff-pcall" + case 7: + funcName = "ff-pcall-hook" + } + frames.Append(&libpf.Frame{ + Type: libpf.LuaJITFrame, + FunctionName: libpf.Intern("LuaJIT FFI: " + funcName), + }) + return nil + case support.LJGReport: + if frame.NumVariables() < 1 { + return errors.New("LuaJIT G report frame not large enough") + } + g := libpf.Address(frame.Variable(0)) + if g != 0 { + unseen := l.addVM(g) + if unseen { + log.Infof("New LuaJIT instance detected: %v", g) + if l.ebpf.CoredumpTest() { + return interpreter.ErrLJRestart + } + } + } + return nil + default: + return fmt.Errorf("Unrecognized LuaJIT frame kind: %d", ljkind) + } + + return nil +} + +func (l *luajitInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + return nil, nil +} + +func (l *luajitInstance) ReleaseResources() error { + return nil +} + +func (l *luajitInstance) UpdateLibcInfo(ebpf interpreter.EbpfHandler, pid libpf.PID, info libc.LibcInfo) error { + return nil +} diff --git a/interpreter/luajit/luajit_test.go b/interpreter/luajit/luajit_test.go new file mode 100644 index 000000000..8aab5b1c2 --- /dev/null +++ b/interpreter/luajit/luajit_test.go @@ -0,0 +1,263 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit_test + +import ( + "context" + "io" + "net" + "net/http" + "runtime" + "strconv" + "sync" + "testing" + "time" + + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/require" + testcontainers "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/testutils" + "go.opentelemetry.io/ebpf-profiler/tracer" + tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" +) + +// Run +func TestIntegration(t *testing.T) { + if !testutils.IsRoot() { + t.Skip("root privileges required") + } + // TODO: + // can we make sure the native function above main is the right openresty function? + // can we make sure the native function at the leaf are right (ie for pcre)? + // repeat tests with jit on/off + for _, tc := range []struct { + resource string + structure []string + }{ + {"fib", []string{ + "main", + "u:run_duration", + "f", + "fib:calc", + "Fibonacci:naive", + "inner", + }}, + {"comp", []string{ + "main", + "u:run_duration", + "f", + "c:comp", + "lzw:compress", + }}, + // TODO: Do we want to checkin the cjson.so or can we load it dynamically? + // {"json", []string{ + // "main", + // "u:run_duration", + // "f", + // "n:call", + // "pcall", + + // // "cjson:decode", + // }}, + // TODO: get the unwinding working across ffi callbacks. + // {"ffi", []string{ + // "main", + // "u:run_duration", + // "f", + // "q:sort", + // "ffi:C:qsort", + // }}, + } { + t.Run(tc.resource, func(t *testing.T) { + for _, tag := range []string{ + "1.17.8.2-alpine", + "1.19.9.1-alpine", + "1.21.4.3-buster", + "1.25.3.2-bullseye", + "jammy", + "alpine", + } { + t.Run(tag, func(t *testing.T) { + image := "openresty/openresty:" + tag + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + cont := startContainer(ctx, t, image) + + h, err := cont.Host(ctx) + require.NoError(t, err) + + port, err := cont.MappedPort(ctx, "8080") + require.NoError(t, err) + + enabledTracers, err := tracertypes.Parse("luajit") + require.NoError(t, err) + enabledTracers.Enable(tracertypes.LuaJITTracer) + traceCh, trc := testutils.StartTracer(ctx, t, enabledTracers, false) + + var waitGroup sync.WaitGroup + defer waitGroup.Wait() + makeRequests(ctx, t, &waitGroup, tc.resource, h, port) + + st, err := cont.State(ctx) + require.NoError(t, err) + + passes, fails, traces := 0, 0, 0 + tick := time.NewTicker(5 * time.Second) + done: + for { + select { + case <-tick.C: + t.Log("passes", passes, "fails", fails, "total", traces) + case <-ctx.Done(): + break done + case traceEvent := <-traceCh: + // See if PID is openresty + trace := traceEvent.Trace + meta := traceEvent.Meta + if trace == nil || int(meta.PID) != st.Pid { + continue + } + traces++ + if validateTrace(t, trc, trace, tc.structure) { + passes++ + } else { + fails++ + } + if passes > 1 { + break done + } + } + } + + t.Log("passes", passes, "fails", fails, "total", traces) + cancel() + }) + } + }) + } +} + +// Find lua traces and test that they are good +func validateTrace(t *testing.T, trc *tracer.Tracer, trace *libpf.Trace, + st []string) bool { + + return validateFrames(t, trace.Frames, st) +} + +func validateFrames(t *testing.T, frames libpf.Frames, st []string) bool { + j := len(frames) - 1 +outer: + for _, s := range st { + if s[0] == '@' { + a, err := strconv.ParseInt(s[1:], 16, 64) + require.NoError(t, err) + addr := libpf.AddressOrLineno(uint64(a)) + for ; j >= 0; j-- { + if frames[j].Value().Type == libpf.NativeFrame { + if frames[j].Value().AddressOrLineno == addr { + continue outer + } + } + } + } else { + for ; j >= 0; j-- { + sym := frames[j].Value().FunctionName.String() + if sym == s { + continue outer + } + } + } + return false + } + return true +} + +func startContainer(ctx context.Context, t *testing.T, image string) testcontainers.Container { + t.Log("starting", image) + // The offset tests load both platform images so docker gets confused if we don't specify + var platform string + switch runtime.GOARCH { + case "arm64": + platform = "linux/arm64" + case "amd64": + platform = "linux/amd64" + default: + panic("bad architecture") + } + cont, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: image, + ExposedPorts: []string{"8080"}, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: "./testdata/nginx.conf", + ContainerFilePath: "/usr/local/openresty/nginx/conf/nginx.conf", + }, + { + HostFilePath: "./testdata/lua", + ContainerFilePath: "/usr/local/openresty/nginx/lua", + }, + }, + ImagePlatform: platform, + WaitingFor: wait.ForHTTP("/"), + }, + Started: true, + }) + require.NoError(t, err) + return cont +} + +func makeRequests(ctx context.Context, t *testing.T, wg *sync.WaitGroup, + res, h string, p nat.Port) { + wg.Add(1) + numRequests := 0 + tick := time.NewTicker(5 * time.Second) + go func() { + defer wg.Done() + for { + select { + case <-tick.C: + t.Log("requests: ", numRequests) + case <-ctx.Done(): + return + default: + } + url := "http://" + net.JoinHostPort(h, p.Port()) + "/" + res + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + t.Log(err) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + time.Sleep(100 * time.Millisecond) + t.Log(err) + continue + } + body, err := io.ReadAll(res.Body) + if err != nil { + t.Log(err) + } + err = res.Body.Close() + if err != nil { + t.Log(err) + } + showContents := false + if showContents { + t.Log(string(body)) + } + numRequests++ + } + }() +} diff --git a/interpreter/luajit/mappings_test.go b/interpreter/luajit/mappings_test.go new file mode 100644 index 000000000..b4b92f35d --- /dev/null +++ b/interpreter/luajit/mappings_test.go @@ -0,0 +1,83 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +package luajit + +import ( + "debug/elf" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/interpreter" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/lpm" + "go.opentelemetry.io/ebpf-profiler/process" +) + +type prefixKey struct { + pid libpf.PID + pfx lpm.Prefix +} + +// ebpfMapsMockup implements the ebpf interface as test mockup +type ebpfMapsMockup struct { + interpreter.EbpfHandlerStubs + prefixes map[prefixKey]lpm.Prefix +} + +var _ interpreter.EbpfHandler = &ebpfMapsMockup{} + +func (m *ebpfMapsMockup) UpdatePidInterpreterMapping(pid libpf.PID, + pfx lpm.Prefix, _ uint8, _ host.FileID, _ uint64) error { + m.prefixes[prefixKey{pid: pid, pfx: pfx}] = pfx + return nil +} + +func (m *ebpfMapsMockup) DeletePidInterpreterMapping(pid libpf.PID, pfx lpm.Prefix) error { + delete(m.prefixes, prefixKey{pid: pid, pfx: pfx}) + return nil +} + +// TestSynchronizeMappings tests that if a mapping is realloc'd we do the right thing. +func TestSynchronizeMappings(t *testing.T) { + for _, tc := range []struct { + calls []process.Mapping + }{ + {[]process.Mapping{ + {Vaddr: 0x2000, Length: 0x1000, Flags: elf.PF_X}, + {Vaddr: 0x1000, Length: 0x2000, Flags: elf.PF_X}, + }}, + {[]process.Mapping{ + {Vaddr: 0x2000, Length: 0x1000, Flags: elf.PF_X}, + {Vaddr: 0x2000, Length: 0x2000, Flags: elf.PF_X}, + }}, + } { + ebpf := &ebpfMapsMockup{prefixes: make(map[prefixKey]lpm.Prefix)} + lj := &luajitInstance{ + jitRegions: make(regionMap), + prefixes: make(map[regionKey][]lpm.Prefix), + prefixesByG: make(map[libpf.Address][]lpm.Prefix), + } + for _, call := range tc.calls { + err := lj.synchronizeMappings(ebpf, 0, []process.Mapping{call}) + require.NoError(t, err) + } + initial := tc.calls[0] + require.Empty(t, lj.jitRegions[initial]) + require.Empty(t, lj.prefixes[regionKey{initial.Vaddr, initial.Vaddr + initial.Length}]) + final := tc.calls[len(tc.calls)-1] + require.NotEmpty(t, lj.jitRegions[final]) + require.NotEmpty(t, lj.prefixes[regionKey{final.Vaddr, final.Vaddr + final.Length}]) + err := lj.Detach(ebpf, 0) + require.NoError(t, err) + require.Empty(t, ebpf.prefixes) + } +} diff --git a/interpreter/luajit/offsets.go b/interpreter/luajit/offsets.go new file mode 100644 index 000000000..ebb92d82a --- /dev/null +++ b/interpreter/luajit/offsets.go @@ -0,0 +1,407 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import ( + "debug/elf" + "fmt" + "slices" + + "go.opentelemetry.io/ebpf-profiler/internal/log" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe" + "go.opentelemetry.io/ebpf-profiler/util" +) + +func scanSymbols(ef *pfelf.File) map[libpf.SymbolName]libpf.Symbol { + interestingSymbols := map[libpf.SymbolName]struct{}{ + "lj_vm_asm_begin": {}, + "jit_checktrace": {}, + "lj_cf_jit_util_traceinfo": {}, + "lua_pushcclosure": {}, + "luaopen_jit": {}, + "lua_close": {}, + } + + foundSymbols := map[libpf.SymbolName]libpf.Symbol{} + + visitor := func(sym libpf.Symbol) bool { + if _, ok := interestingSymbols[sym.Name]; ok { + foundSymbols[sym.Name] = sym + delete(interestingSymbols, sym.Name) + } + return len(interestingSymbols) > 0 + } + if err := ef.VisitSymbols(visitor); err != nil { + log.Warnf("failed to read symbols: %v", err) + } + + if err := ef.VisitDynamicSymbols(visitor); err != nil { + log.Warnf("failed to read dynamic symbols: %v", err) + } + + return foundSymbols +} + +// This is the main "global" struct in luajit. +// +// type = struct GG_State { +// lua_State L; +// global_State g; +// jit_State J; +// HotCount hotcount[64]; +// ASMFunction dispatch[243]; +// BCIns bcff[57]; +// } +// +// All the code here is to enable us navigate around it. We need: +// +// 1. The distance from g to dispatch +// 2. The distance from g to J.trace +// 3. The offset of cur_L in global_State +// +// Some versions of openresty have a stripped luajit which makes things a little more +// complicated because we have to start from a public symbol and work our way around. +func extractOffsets(ef *pfelf.File, ljd *luajitData, ir util.Range) error { + oft := offsetData{} + if err := oft.init(ef); err != nil { + return err + } + + curLOffset, err := oft.findCurLOffset() + if err != nil { + return err + } + if curLOffset > 0x7fff { + return fmt.Errorf("lj: curL offset %v is too large", curLOffset) + } + ljd.currentLOffset = curLOffset + + g2Traces, err := oft.findG2TracesOffset() + if err != nil { + return fmt.Errorf("lj: failed to find g2traces offset: %v", err) + } + if g2Traces > 0xffff { + return fmt.Errorf("lj: g to traces offset %v is too large", g2Traces) + } + ljd.g2Traces = uint16(g2Traces) + + g2dispatch, err := oft.findG2DispatchOffset() + if err != nil { + return err + } + if g2dispatch > 0xffff { + return fmt.Errorf("lj: dispatch_L offset %v is too large", g2dispatch) + } + ljd.g2Dispatch = uint16(g2dispatch) + + // If we have symbols we can check that the start address is correct. + if s, e := oft.lookupSymbol("lj_vm_asm_begin"); e == nil && ir.Start != uint64(s.Address) { + return fmt.Errorf("lj: unexpected start address %x, expected %x", s.Address, ir.Start) + } + + return nil +} + +type extractor interface { + // LUA_API void lua_close(lua_State *L) + // { + // global_State *g = G(L); + // int i; + // L = mainthread(g); /* Only the main thread can be closed. */ + // + // #if LJ_HASPROFILE + // luaJIT_profile_stop(L); + // #endif + // + // setgcrefnull(g->cur_L); <---- DING DING DING + findOffsetsFromLuaClose(b []byte) (uint64, uint64, error) + + // Find call to lj_dispatch_update in luaopen_jit by looking for + // first call being passed G loaded from L->glref. + findLjDispatchUpdateAddr(b []byte, addr uint64) (uint64, error) + + // luaopen_jit calls jit_init which calls lj_dispatch_update. lj_dispatch_update + // has this line near the beginning: + // ASMFunction *disp = G2GG(g)->dispatch; + // Use this line to find the g2dispatch offset. + findG2DispatchOffsetFromLjDispatchUpdate(b []byte) (uint64, error) + + // jit_checktrace does this: + // + // jit_State *J = L2J(L); + // if (tr > 0 && tr < J->sizetrace) + // return traceref(J, tr); + // + // L2J will find J relative to G and traceref will find traces + // relative to J so we find both offsets and add them to get + // g2traces offset. + findG2TracesOffsetFromChecktrace([]byte) (uint64, error) + + // Return true if the code in b calls targetCall. + callExists(b []byte, baseAddr, targetCall int64) (bool, error) + + findFirstCall(b []byte, baseAddr int64) (uint64, error) + + find3rdArgToLibPreregCall(b []byte, baseAddr int64) (uint64, error) + + find4thArgToLibRegCall(b []byte, baseAddr int64) (int64, error) +} + +func newExtractor(ef *pfelf.File) extractor { + switch ef.Machine { + case elf.EM_X86_64: + return &x86Extractor{ef: ef} + case elf.EM_AARCH64: + return &armExtractor{ef: ef} + default: + panic("unexpected architecture") + } +} + +type offsetData struct { + f *pfelf.File + luajitOpen []byte + luajitOpenAddr uint64 + e extractor + foundSymbols map[libpf.SymbolName]libpf.Symbol +} + +func (o *offsetData) init(ef *pfelf.File) error { + o.f = ef + o.e = newExtractor(ef) + + var err error + o.foundSymbols = scanSymbols(ef) + // Two extractors use luaopen_jit so cache it. + b, addr, err := o.readSymByName("luaopen_jit") + if err != nil { + return err + } + o.luajitOpen = b + o.luajitOpenAddr = uint64(addr) + return nil +} + +func (o *offsetData) findCurLOffset() (uint16, error) { + b, _, err := o.readSymByName("lua_close") + if err != nil { + return 0, err + } + glref, curL, err := o.e.findOffsetsFromLuaClose(b) + if err != nil { + return 0, err + } + + // openresty 1.15 was compiled w/o LJ_GC64 which we don't support. + if glref != 0x10 { + //nolint: lll + return 0, fmt.Errorf("unexpected glref offset %x, only luajit with LJ_GC64 is supported", glref) + } + return uint16(curL), nil +} + +func (o *offsetData) findG2DispatchOffset() (uint64, error) { + luaDispatchUpdateAddr, err := o.e.findLjDispatchUpdateAddr(o.luajitOpen, o.luajitOpenAddr) + if err != nil { + return 0, err + } + b := make([]byte, 300) + _, err = o.f.ReadAt(b, int64(luaDispatchUpdateAddr)) + if err != nil { + return 0, err + } + return o.e.findG2DispatchOffsetFromLjDispatchUpdate(b) +} + +func (o *offsetData) findG2TracesOffset() (uint64, error) { + if sym, err := o.lookupSymbol("jit_checktrace"); err == nil { + // easiest case + b, err := o.readSym(sym) + if err != nil { + return 0, err + } + return o.e.findG2TracesOffsetFromChecktrace(b) + } + + // jit_checktrace could be inlined or we could be dealing with a stripped binary + if sym, err := o.lookupSymbol("lj_cf_jit_util_traceinfo"); err == nil { + // Inline case + b, er := o.readSym(sym) + if er != nil { + return 0, er + } + return o.e.findG2TracesOffsetFromChecktrace(b) + } + + // Binary must be stripped, find traceinfo the hard way. + sym, err := o.findTraceInfoFromLuaOpen() + if err != nil { + return 0, err + } + + b, err := o.readSym(sym) + if err != nil { + return 0, err + } + + // jit_checktrace will be first call in lj_cf_jit_util_traceinfo or it will be inlined, + // first try the inline approach by running find on a small subset of the instructions. + if len(b) > 200 { + b = b[:200] + } + + addr, err := o.e.findG2TracesOffsetFromChecktrace(b) + if err != nil { + callAddr, err := o.e.findFirstCall(b, int64(sym.Address)) + if err != nil { + return 0, err + } + b := make([]byte, 100) + _, err = o.f.ReadAt(b, int64(callAddr)) + if err != nil { + return 0, err + } + addr, err = o.e.findG2TracesOffsetFromChecktrace(b) + if err != nil { + return 0, err + } + } + + return addr, nil +} + +// Get address of lj_cf_jit_util_traceinfo by looking at the lj_lib_prereg call in luaopen_jit: +// https://github.com/openresty/luajit2/blob/7952882d/src/lib_jit.c#L803 +// The lj_lib_prereg call may or may not be inlined which we can detect by looking for a call to the +// public lua_pushcclosure method. In either case we need to get the address of "luaopen_jit_util" +// which will be an argument to lj_lib_prereg, or lj_pushclosure. +// Then we can read that function to find the address of the function array "lj_lib_cf_jit_util" +// which will be an argument to lj_lib_register. Finally the lj_cf_jit_util_traceinfo function +// will be the 4th element of that array. +func (o *offsetData) findTraceInfoFromLuaOpen() (*libpf.Symbol, error) { + pushCClosure, err := o.lookupSymbol("lua_pushcclosure") + if err != nil { + return nil, err + } + pushClosureAddr := int64(pushCClosure.Address) + baseAddr := int64(o.luajitOpenAddr) + var luaopenJitUtilAddr uint64 + inlined, err := o.e.callExists(o.luajitOpen, baseAddr, pushClosureAddr) + if err != nil { + return nil, err + } + + if inlined { + luaopenJitUtilAddr, err = findRipRelativeLea2ndArgTo2ndCall(o.luajitOpen, baseAddr, + pushClosureAddr) + if err != nil { + return nil, err + } + } else { + luaopenJitUtilAddr, err = o.e.find3rdArgToLibPreregCall(o.luajitOpen, baseAddr) + if err != nil { + return nil, err + } + } + logf("lj: luaopen_jit_util address %x", luaopenJitUtilAddr) + // luaopen_jit_util is tiny: + // https://github.com/openresty/luajit2/blob/7952882d9c/src/lib_jit.c#L484 + b := make([]byte, 100) + _, err = o.f.ReadAt(b, int64(luaopenJitUtilAddr)) + if err != nil { + return nil, err + } + libJitFunctionAddresses, err := o.e.find4thArgToLibRegCall(b, int64(luaopenJitUtilAddr)) + if err != nil { + return nil, err + } + + // libJitFunctionAddresses should be this static array: + // No permalinks for generated code, its in lj_libdef.h + // static const lua_CFunction lj_lib_cf_jit_util[] = { + // lj_cf_jit_util_funcinfo, + // lj_cf_jit_util_funcbc, + // lj_cf_jit_util_funck, + // lj_cf_jit_util_funcuvname, + // lj_cf_jit_util_traceinfo, + // lj_cf_jit_util_traceir, + // lj_cf_jit_util_tracek, + // lj_cf_jit_util_tracesnap, + // lj_cf_jit_util_tracemc, + // lj_cf_jit_util_traceexitstub, + // lj_cf_jit_util_ircalladdr + // }; + const traceInfoIndex = 4 + funcAddrs := make([]uint64, 12) + _, err = o.f.ReadAt(pfunsafe.FromSlice(funcAddrs), libJitFunctionAddresses) + if err != nil { + return nil, err + } + + traceInfoAddr := funcAddrs[traceInfoIndex] + + // Derive size by sorting and seeing offset to next function, swag if its last (it won't be). + slices.Sort(funcAddrs) + + // Its a tiny function, give it reasonable default. + traceInfoSize := uint64(100) + for i, addr := range funcAddrs { + if addr == traceInfoAddr && i != len(funcAddrs)-1 { + traceInfoSize = funcAddrs[i+1] - funcAddrs[i] + break + } + } + + return &libpf.Symbol{ + Name: "lj_cf_jit_util_traceinfo", + Address: libpf.SymbolValue(traceInfoAddr), + Size: traceInfoSize}, nil +} + +func (o *offsetData) readSym(sym *libpf.Symbol) ([]byte, error) { + b := make([]byte, sym.Size) + n, err := o.f.ReadAt(b, int64(sym.Address)) + if err != nil { + return nil, err + } + if n != len(b) { + return nil, fmt.Errorf("failed to read %s fully", sym.Name) + } + return b, nil +} + +func (o *offsetData) lookupSymbol(name libpf.SymbolName) (s *libpf.Symbol, err error) { + s, err = o.f.LookupSymbol(name) + if err == libpf.ErrSymbolNotFound && o.foundSymbols != nil { + if sym, ok := o.foundSymbols[name]; ok { + s = &sym + err = nil + } + } + return s, err +} + +//nolint:gocritic +func (o *offsetData) readSymByName(name string) ([]byte, int64, error) { + sym, err := o.lookupSymbol(libpf.SymbolName(name)) + if err != nil { + return nil, 0, err + } + b := make([]byte, sym.Size) + _, err = o.f.ReadAt(b, int64(sym.Address)) + if err != nil { + return nil, 0, err + } + return b, int64(sym.Address), nil +} diff --git a/interpreter/luajit/offsets_arm64.go b/interpreter/luajit/offsets_arm64.go new file mode 100644 index 000000000..efe162c01 --- /dev/null +++ b/interpreter/luajit/offsets_arm64.go @@ -0,0 +1,23 @@ +//go:build arm64 + +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import "go.opentelemetry.io/ebpf-profiler/support" + +const ( + cframeSize int32 = support.LJCframeSpaceArm + cframeSizeJIT int32 = cframeSize + 16 +) diff --git a/interpreter/luajit/offsets_test.go b/interpreter/luajit/offsets_test.go new file mode 100644 index 000000000..e7a781dad --- /dev/null +++ b/interpreter/luajit/offsets_test.go @@ -0,0 +1,328 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit + +import ( + "context" + "debug/dwarf" + "debug/elf" + "io" + "os" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" + testcontainers "github.com/testcontainers/testcontainers-go" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" + sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" +) + +const ( + openrestyBase = "openresty/openresty" +) + +func TestOffsets(t *testing.T) { + for _, tc := range []struct { + tag string + suf string + fail bool + }{ + {"1.13.6.2-alpine", "0", true}, + {"1.15.8.3-alpine", "0", false}, + {"1.17.8.2-alpine", "0", false}, + {"1.19.9.1-focal", "0", false}, + {"1.21.4.3-buster-fat", "0", false}, + {"1.21.4.3-alpine", "0", false}, + {"1.25.3.2-bullseye-fat", "ROLLING", false}, + {"1.25.3.2-alpine", "ROLLING", false}, + {"jammy", "ROLLING", false}, + {"alpine", "ROLLING", false}, + } { + for _, platform := range []string{"linux/amd64", "linux/arm64"} { + tag, suffix := tc.tag, tc.suf + libFile := "libluajit-5.1.so.2.1." + suffix + t.Run(tag+"-"+platform, func(t *testing.T) { + target, noarm := cacheLibrary(t, tag, platform, libFile) + if noarm { + t.Skip("old openresty doesn't have arm") + } + + ef, err := pfelf.Open(target) + require.NoError(t, err) + + // create stacktrace deltas to make sure we can find interp bounds + // some ugliness so we can run arm and x86 unit tests on both platforms. + intervals, param, err := extractStackDeltas(target, ef) + require.NoError(t, err) + + interp, err := extractInterpreterBounds(intervals.Deltas, param) + require.NoError(t, err) + + ljd := luajitData{} + err = extractOffsets(ef, &ljd, interp) + + if tc.fail { + //nolint:lll + require.Error(t, err, "unexpected glref offset 8, only luajit with LJ_GC64 is supported") + return + } + + require.NoError(t, err) + require.NotZero(t, ljd.currentLOffset) + require.NotZero(t, ljd.g2Traces) + require.NotZero(t, ljd.g2Dispatch) + + od := offsetData{} + err = od.init(ef) + require.NoError(t, err) + + // Test that our chicanery for finding traceinfo checks out on symbolized builds. + if ti, err1 := od.lookupSymbol("lj_cf_jit_util_traceinfo"); err1 == nil { + ti2, err2 := od.findTraceInfoFromLuaOpen() + require.NoError(t, err2) + require.Equal(t, ti.Address, ti2.Address) + } + + // Ditto for lj_dispatch_update + if du, err1 := od.lookupSymbol("lj_dispatch_update"); err1 == nil { + du2, err2 := od.e.findLjDispatchUpdateAddr(od.luajitOpen, od.luajitOpenAddr) + require.NoError(t, err2) + require.Equal(t, uint64(du.Address), du2) + } + + // TODO: strip binary and do it again. + }) + } + } +} + +func cacheLibrary(t *testing.T, tag, platform, libFile string) (string, bool) { + baseDir := "/tmp/offsets_artifacts/" + tag + "/" + platform + target := baseDir + "/libluajit-5.1.so" + + if strings.HasPrefix(tag, "1.13") || strings.HasPrefix(tag, "1.15") { + if platform == "linux/arm64" { + return "", true + } + } + + if _, err := os.Stat(target); os.IsNotExist(err) { + err = os.MkdirAll(baseDir, 0o755) + require.NoError(t, err) + getLibFromImage(t, openrestyBase+":"+tag, platform, libFile, target) + } + return target, false +} + +func extractStackDeltas(target string, ef *pfelf.File) (sdtypes.IntervalData, int32, error) { + var intervals sdtypes.IntervalData + if err := elfunwindinfo.Extract(target, &intervals); err != nil { + return intervals, 0, err + } + + var param int32 + switch ef.Machine { + case elf.EM_AARCH64: + param = 208 + case elf.EM_X86_64: + param = 80 + } + return intervals, param, nil +} + +func getLibFromImage(t *testing.T, name, platform, fullPath, target string) { + ctx, cancel := context.WithCancel(t.Context()) + t.Cleanup(cancel) + + image, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: name, + ImagePlatform: platform, + }, + Started: false, + }) + require.NoError(t, err) + + rc, err := image.CopyFileFromContainer(ctx, "/usr/local/openresty/luajit/lib/"+fullPath) + require.NoError(t, err) + defer rc.Close() + f, err := os.Create(target) + require.NoError(t, err) + + _, err = io.Copy(f, rc) + require.NoError(t, err) +} + +func TestX86LuaClose(t *testing.T) { + testdata := []struct { + name string + glRefExpected uint64 + curLExpected uint64 + code []byte + }{ + { + name: "size-optimized-register-zero", + glRefExpected: 0x10, + curLExpected: 0x158, + code: []byte{ + 0x41, 0x55, // pushq %r13 + 0x4c, 0x8d, 0x2d, 0x3f, 0xd4, 0xff, 0xff, // leaq -0x2bc1(%rip), %r13 + 0x41, 0x54, // pushq %r12 + 0x41, 0xbc, 0x0a, 0x00, 0x00, 0x00, // movl $0xa, %r12d + 0x55, // pushq %rbp + 0x53, // pushq %rbx + 0x51, // pushq %rcx + 0x48, 0x8b, 0x5f, 0x10, // movq 0x10(%rdi), %rbx + 0x48, 0x8b, 0xab, 0xc8, 0x00, 0x00, 0x00, // movq 0xc8(%rbx), %rbp + 0x48, 0x89, 0xef, // movq %rbp, %rdi + 0xe8, 0x6e, 0x17, 0x00, 0x00, // callq 0x175f0 + 0x31, 0xf6, // xorl %esi, %esi + 0x48, 0x89, 0xef, // movq %rbp, %rdi + 0x48, 0x89, 0xb3, 0x58, 0x01, 0x00, 0x00, // movq %rsi, 0x158(%rbx) + }, + }, + } + + for _, test := range testdata { + x := x86Extractor{} + glref, curL, err := x.findOffsetsFromLuaClose(test.code) + require.NoError(t, err) + require.Equal(t, test.glRefExpected, glref) + require.Equal(t, test.curLExpected, curL) + } +} + +// spot testing +func TestFiles(t *testing.T) { + files, err := os.ReadDir("./testdata") + require.NoError(t, err) + for _, de := range files { + target := "./testdata/" + de.Name() + fi, err := os.Stat(target) + if err != nil || fi.IsDir() { + continue + } + ef, err := pfelf.Open(target) + // Skip non-elf files + if err != nil { + continue + } + ljd := luajitData{} + + // create stacktrace deltas to make sure we can find interp bounds + // some ugliness so we can run arm and x86 unit tests on both platforms. + intervals, param, err := extractStackDeltas(target, ef) + require.NoError(t, err) + + interp, err := extractInterpreterBounds(intervals.Deltas, param) + require.NoError(t, err) + + err = extractOffsets(ef, &ljd, interp) + require.NoError(t, err, de) + require.NotZero(t, ljd.currentLOffset) + require.NotZero(t, ljd.g2Traces) + require.NotZero(t, ljd.g2Dispatch) + + od := offsetData{} + err = od.init(ef) + require.NoError(t, err) + + // Test that our chicanery for finding traceinfo checks out on symbolized builds. + if ti, err1 := od.lookupSymbol("lj_cf_jit_util_traceinfo"); err1 == nil { + ti2, err2 := od.findTraceInfoFromLuaOpen() + require.NoError(t, err2) + require.Equal(t, ti.Address, ti2.Address) + } + + // Ditto for lj_dispatch_update + if du, err1 := od.lookupSymbol("lj_dispatch_update"); err1 == nil { + du2, err2 := od.e.findLjDispatchUpdateAddr(od.luajitOpen, od.luajitOpenAddr) + require.NoError(t, err2) + require.Equal(t, uint64(du.Address), du2) + } + + t.Logf("%s: %+v, interp: %+v", target, ljd, interp) + } +} + +func TestStructure(t *testing.T) { + for _, tc := range []struct { + tag string + suf string + }{ + // Seems like alpine and ubuntu always have symbols, debian doesn't + {"1.15.8.3-alpine", "0"}, + {"1.17.8.2-alpine", "0"}, + {"1.19.9.1-focal", "0"}, + {"1.21.4.3-alpine", "0"}, + {"1.25.3.2-alpine", "ROLLING"}, + {"jammy", "ROLLING"}, + {"alpine", "ROLLING"}, + } { + for _, platform := range []string{"linux/amd64", "linux/arm64"} { + tag, suffix := tc.tag, tc.suf + libFile := "libluajit-5.1.so.2.1." + suffix + t.Run(tag+"-"+platform, func(t *testing.T) { + target, noarm := cacheLibrary(t, tag, platform, libFile) + if noarm { + t.Skip("old openresty doesn't have arm") + } + + ef, err := elf.Open(target) + require.NoError(t, err) + + dwarfData, err := ef.DWARF() + require.NoError(t, err) + entryReader := dwarfData.Reader() + + for { + entry, err := entryReader.Next() + require.NoError(t, err) + if entry == nil { + break + } + if entry.Tag == dwarf.TagStructType { + ty, err := dwarfData.Type(entry.Offset) + require.NoError(t, err) + if s, ok := ty.(*dwarf.StructType); ok { + switch s.StructName { + case "GCtrace": + checkStruct(t, trace{}, s, tracePartOffset) + case "GCproto": + checkStruct(t, protoRaw{}, s, 8) + case "jit_State": + // TODO: we don't have offset as we rely on g2traces so not sure + // how to test... + } + } + } + } + }) + } + } +} + +func checkStruct(t *testing.T, typ any, s *dwarf.StructType, base uintptr) { + rtyp := reflect.TypeOf(typ) + did := 0 + for i := 0; i < rtyp.NumField(); i++ { + f := rtyp.Field(i) + if f.Name != "_" { + for s.Field[did].Name != f.Name { + did++ + } + require.Equal(t, s.Field[did].ByteOffset, int64(f.Offset+base)) + } + } +} diff --git a/interpreter/luajit/offsets_x86.go b/interpreter/luajit/offsets_x86.go new file mode 100644 index 000000000..09f784cdb --- /dev/null +++ b/interpreter/luajit/offsets_x86.go @@ -0,0 +1,23 @@ +//go:build amd64 + +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import "go.opentelemetry.io/ebpf-profiler/support" + +const ( + cframeSize int32 = support.LJCframeSpaceX86 + cframeSizeJIT int32 = cframeSize + 16 +) diff --git a/interpreter/luajit/proto.go b/interpreter/luajit/proto.go new file mode 100644 index 000000000..4ab8de5c5 --- /dev/null +++ b/interpreter/luajit/proto.go @@ -0,0 +1,371 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import ( + "errors" + "unicode/utf8" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe" + "go.opentelemetry.io/ebpf-profiler/remotememory" +) + +const ( + sizeofGCstr = 24 + sizeofGCproto = 104 + stringGCType = 4 + //https://github.com/openresty/luajit2/blob/7952882d/src/lj_def.h#L66 + byteCodeMax = 1 << 26 +) + +type GCobj struct { + _ uint64 // nextgc + _ byte // marked + gct byte +} + +// GCproto minus first 8 bytes +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_obj.h#L372 +// All the pointers (except chunkname) are pointers to extra space at the end of the GCproto object +// so we could try to be clever and read the whole thing at once if we needed to reduce remotememory +// traffic. +type protoRaw struct { + // nextgc uint64 /* 0 | 8 */ + _ byte /* 8 | 1 */ + _ byte /* 9 | 1 */ + _ byte /* 10 | 1 */ + _ byte /* 11 | 1 */ + sizebc uint32 /* 12 | 4 */ + _ uint32 /* 16 | 4 */ + /* XXX 4-byte hole */ + _ uint64 /* 24 | 8 */ + k libpf.Address /* 32 | 8 */ + _ uint64 /* 40 | 8 */ + sizekgc uint32 /* 48 | 4 */ + _ uint32 /* 52 | 4 */ + sizept uint32 /* 56 | 4 */ + sizeuv uint8 /* 60 | 1 */ + _ uint8 /* 61 | 1 */ + _ uint16 /* 62 | 2 */ + chunkname libpf.Address /* 64 | 8 */ + firstline uint32 /* 72 | 4 */ + numline uint32 /* 76 | 4 */ + lineinfo libpf.Address /* 80 | 8 */ + uvinfo libpf.Address /* 88 | 8 */ + varinfo libpf.Address /* 96 | 8 */ +} + +// proto is a userland cached version of LuaJIT's GCproto object which is +// contains all the static data for a function. A function on the stack +// will be a GCfunc which is basically a GCproto pointer and any captured +// upvalues. +type proto struct { + protoRaw + ptAddr libpf.Address + name string + bc []uint32 + lineinfo8 []uint8 + lineinfo16 []uint16 + lineinfo32 []uint32 + upvalueNames []string + varinforaw []byte + constants []string +} + +// newProto creates a proto from a GCproto* by reading memory remotely. +func newProto(rm remotememory.RemoteMemory, pt libpf.Address) (*proto, error) { + p := &proto{ptAddr: pt} + if err := rm.Read(pt+8, pfunsafe.FromPointer(&p.protoRaw)); err != nil { + return nil, err + } + + // reading memory from a remote process is always dicey, validate + // we're looking at a GCproto object by checking that the debugging + // info pointers are valid internal pointers or NULL. + end := pt + libpf.Address(p.sizept) + bad := func(addr libpf.Address) bool { + return addr != 0 && (addr < pt || addr >= end) + } + if bad(p.lineinfo) || bad(p.uvinfo) || bad(p.varinfo) { + return nil, errors.New("invalid GCproto object") + } + + // string data is stored after the GCstr object + p.name = rm.String(p.chunkname + sizeofGCstr) + if !utf8.ValidString(p.name) { + return nil, errors.New("invalid chunkname string") + } + + // This should never be empty string. + if p.name == "" { + return nil, errors.New("invalid chunkname string") + } + + if p.sizebc == 0 || p.sizebc > byteCodeMax { + return nil, errors.New("invalid bytecode size") + } + + p.bc = make([]uint32, p.sizebc) + // bytecode starts at end of GCproto object + // https://github.com/openresty/luajit2/blob/7952882d/src/lj_obj.h#L420 + if err := rm.Read(p.ptAddr+sizeofGCproto, pfunsafe.FromSlice(p.bc)); err != nil { + return nil, err + } + if p.lineinfo != 0 { + if p.numline < 256 { + p.lineinfo8 = make([]uint8, p.sizebc) + if err := rm.Read(p.lineinfo, pfunsafe.FromSlice(p.lineinfo8)); err != nil { + return nil, err + } + } else if p.numline < 65536 { + p.lineinfo16 = make([]uint16, p.sizebc) + if err := rm.Read(p.lineinfo, pfunsafe.FromSlice(p.lineinfo16)); err != nil { + return nil, err + } + } else { + p.lineinfo32 = make([]uint32, p.sizebc) + if err := rm.Read(p.lineinfo, pfunsafe.FromSlice(p.lineinfo32)); err != nil { + return nil, err + } + } + } + + if p.sizekgc > 0 { + objs := make([]libpf.Address, p.sizekgc) + p.constants = make([]string, p.sizekgc) + if err := rm.Read(p.k-libpf.Address(p.sizekgc*8), pfunsafe.FromSlice(objs)); err != nil { + return nil, err + } + for i, c := range objs { + var gco GCobj + if err := rm.Read(c, pfunsafe.FromPointer(&gco)); err != nil { + return nil, err + } + if gco.gct == stringGCType { + str := rm.String(objs[i] + sizeofGCstr) + p.constants[len(objs)-i-1] = str + } + } + } + + //https://github.com/openresty/luajit2/blob/7952882d/src/lj_debug.c#L225 + if p.uvinfo != 0 { + // lineinfo/uvinfo/varinfo are all either null or set so we can calculate lengths from them + lenuv := p.varinfo - p.uvinfo + b := make([]byte, lenuv) + if err := rm.Read(p.uvinfo, pfunsafe.FromSlice(b)); err != nil { + return nil, err + } + p.upvalueNames = []string{} + for len(b) > 0 { + var name string + b, name = parseString(b) + b = b[1:] // skip null terminator + p.upvalueNames = append(p.upvalueNames, name) + } + if p.sizeuv != uint8(len(p.upvalueNames)) { + return nil, errors.New("invalid upvalue count") + } + } + + // varinfo is a pointer to data colocated with GCproto, its at the end + // and its length isn't stored, but it can be derived by subtracting the + // end of the object from the varinfo pointer. + if p.varinfo != 0 { + varinfolen := (p.ptAddr + libpf.Address(p.sizept)) - p.varinfo + p.varinforaw = make([]byte, varinfolen) + if err := rm.Read(p.varinfo, pfunsafe.FromSlice(p.varinforaw)); err != nil { + return nil, err + } + } + + return p, nil +} + +func (p *proto) getName() string { + if p == nil { + return "" + } + return p.name +} + +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_debug.c#L123 +func (p *proto) getLine(pc uint32) uint32 { + if p == nil || p.lineinfo == 0 || pc > p.sizebc || pc == 0 { + return 0 + } + first := p.firstline + if pc == p.sizebc { + return first + p.numline + } + pc-- + if pc == 0 { + return first + } + if p.numline < 256 { + return first + uint32(p.lineinfo8[pc]) + } else if p.numline < 65536 { + return first + uint32(p.lineinfo16[pc]) + } + return first + p.lineinfo32[pc] +} + +func (p *proto) getVarname(slot, pc uint32) string { + return parseVarinfo(p.varinforaw, pc, slot) +} + +func (p *proto) getUpvalueName(slot uint32) string { + return p.upvalueNames[slot] +} + +func (p *proto) getConstant(idx uint32) string { + return p.constants[idx] +} + +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_debug.c#L259 +func (p *proto) getSlotName(pc, slot uint32) string { +restart: + if pc == 0 || pc >= p.sizebc { + return "" + } + name := p.getVarname(slot, pc) + if name != "" { + return name + } + // Walk the lua instructions backwards to find the name used to put the function in the slot + pc-- + for ; pc > 0; pc-- { + ins := p.bc[pc] + op := bcOp(ins) + ra := bcA(ins) + if bcModeAIsBase(op) { + if slot >= ra && (op != BC_KNIL || slot <= bcD(ins)) { + return "" + } + } else if bcModeAIsDst(op) && ra == slot { + switch op { + case BC_MOV: + if ra == slot { + slot = bcD(ins) + goto restart + } + case BC_GGET: + return p.getConstant(bcD(ins)) + case BC_TGETS: + method := p.getConstant(bcC(ins)) + table := p.getSlotName(pc, bcB(ins)) + if table != "" { + return table + ":" + method + } + return method + case BC_UGET: + return p.getUpvalueName(bcD(ins)) + default: + return "" + } + } + } + + return "" +} + +func (p *proto) getFunctionName(pc uint32) string { + if p == nil { + return "main" + } + if pc >= p.sizebc { + // TODO: can we get a better pc for JIT frames? + pc = 0 + } + slot, metaname := getSlotOrMetaname(p.bc[pc]) + if metaname != "" { + return metaname + } + return p.getSlotName(pc, slot) +} + +// Parse a ULEB128 encoded number from a byte slice and return +// remaining bytes and the number. +// +//nolint:gocritic +func parseULEB128(b []byte) ([]byte, uint32) { + v := uint32(b[0]) + b = b[1:] + if v >= 0x80 { + shift := 0 + v &= 0x7f + for { + shift += 7 + v |= uint32(b[0]&0x7f) << shift + b = b[1:] + if b[0] < 0x80 { + break + } + } + } + return b, v +} + +//nolint:gocritic +func parseString(b []byte) ([]byte, string) { + for i, c := range b { + if c == 0 { + // FIXME: allocation + return b[i:], string(b[:i]) + } + } + panic("no null terminator") +} + +var varnames = []string{ + "(for index)", + "(for limit)", + "(for step)", + "(for generator)", + "(for state)", + "(for control)"} + +func parseVarinfo(b []byte, pc, slot uint32) string { + var lastpc uint32 + for { + var name string + vn := int(b[0]) + if vn <= len(varnames) { + if vn == 0 { + break + } + } else { + b, name = parseString(b) + } + b = b[1:] + var pcdelta uint32 + b, pcdelta = parseULEB128(b) + startpc := lastpc + pcdelta + lastpc = startpc + if startpc > pc { + break + } + b, pcdelta = parseULEB128(b) + endpc := startpc + pcdelta + if pc < endpc { + if slot == 0 { + if vn <= len(varnames) { + return varnames[vn-1] + } + return name + } + slot-- + } + } + return "" +} diff --git a/interpreter/luajit/proto_test.go b/interpreter/luajit/proto_test.go new file mode 100644 index 000000000..0b8c8123c --- /dev/null +++ b/interpreter/luajit/proto_test.go @@ -0,0 +1,60 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseVarinfo(t *testing.T) { + // Lifted from bcline function in bc.lua: + // https://github.com/openresty/luajit2/blob/098183d/src/jit/bc.lua#L65 + b := []byte{0x66, 0x75, 0x6e, 0x63, 0x0, 0x0, 0xdc, 0x1, 0x70, 0x63, 0x0, 0x0, 0xdc, 0x1, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x0, 0x0, 0xdc, 0x1, 0x6c, 0x69, 0x6e, 0x65, 0x69, 0x6e, + 0x66, 0x6f, 0x0, 0x0, 0xdc, 0x1, 0x69, 0x6e, 0x73, 0x0, 0xa, 0xd2, 0x1, 0x6d, 0x0, 0x0, + 0xd2, 0x1, 0x6c, 0x0, 0x0, 0xd2, 0x1, 0x6d, 0x61, 0x0, 0xf, 0xc3, 0x1, 0x6d, 0x62, 0x0, + 0x0, 0xc3, 0x1, 0x6d, 0x63, 0x0, 0x0, 0xc3, 0x1, 0x61, 0x0, 0x7, 0xbc, 0x1, 0x6f, 0x69, + 0x64, 0x78, 0x0, 0x5, 0xb7, 0x1, 0x6f, 0x70, 0x0, 0x5, 0xb2, 0x1, 0x73, 0x0, 0x1, 0xb1, + 0x1, 0x64, 0x0, 0x27, 0x8a, 0x1, 0x6b, 0x63, 0x0, 0x17, 0x73, 0x66, 0x69, 0x0, 0x2c, 0x9, + 0x6b, 0x61, 0x0, 0x17, 0x8, 0x62, 0x0, 0xe, 0xf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x79, + 0xf5, 0x0} + for _, tc := range []struct { + name string + startpc, endp uint32 + slot uint32 + }{ + {"func", 0, 220, 0}, + {"pc", 0, 220, 1}, + {"prefix", 0, 220, 2}, + {"lineinfo", 0, 220, 3}, + {"ins", 10, 220, 4}, + {"m", 10, 220, 5}, + {"l", 10, 220, 6}, + {"ma", 25, 220, 7}, + {"mb", 25, 220, 8}, + {"mc", 25, 220, 9}, + {"a", 32, 220, 10}, + {"oidx", 37, 220, 11}, + {"op", 42, 220, 12}, + {"s", 43, 220, 13}, + {"d", 82, 220, 14}, + {"kc", 105, 220, 15}, + {"fi", 149, 158, 16}, + {"ka", 172, 180, 16}, + {"b", 186, 201, 16}, + } { + s := parseVarinfo(b, tc.startpc, tc.slot) + require.Equal(t, s, tc.name) + } +} diff --git a/interpreter/luajit/testdata/lua/comp.lua b/interpreter/luajit/testdata/lua/comp.lua new file mode 100644 index 000000000..24ec1e182 --- /dev/null +++ b/interpreter/luajit/testdata/lua/comp.lua @@ -0,0 +1,18 @@ +local _M = {} +local lzw = require("lualzw") + +function _M.comp(input) + local comp_data = lzw.compress(input) + decomp_data = lzw.decompress(comp_data) + if input ~= decomp_data then + error("Error: input != decomp_data") + end + return comp_data +end + +-- run if outside nginx +if not package.loaded["ngx"] then + print(_M.comp("asdfqwerasdfzcvxpoiulkhasdfasdfkajeofiwjdajfj")) +end + +return _M diff --git a/interpreter/luajit/testdata/lua/fib.lua b/interpreter/luajit/testdata/lua/fib.lua new file mode 100644 index 000000000..b6573bbc5 --- /dev/null +++ b/interpreter/luajit/testdata/lua/fib.lua @@ -0,0 +1,24 @@ +local _M = {} + +local Fibonacci = {} +function Fibonacci.naive(n) + local function inner(m) + if m < 2 then + return m + end + return inner(m-1) + + inner(m-2) + end + return inner(n) +end + +function _M.calc(range) + return "Fib(" .. tostring(range) .. ") = " .. tostring(Fibonacci.naive(range)) +end + +-- run if outside nginx +if not package.loaded["ngx"] then + print(_M.calc(20)) +end + +return _M diff --git a/interpreter/luajit/testdata/lua/input-text.lua b/interpreter/luajit/testdata/lua/input-text.lua new file mode 100644 index 000000000..ef3bb70cc --- /dev/null +++ b/interpreter/luajit/testdata/lua/input-text.lua @@ -0,0 +1,19 @@ +local ffi = require("ffi") +local _M = {} +ffi.cdef[[ + long random(); +]] + +local text = "" + +function _M.gen() + if #text > 0 then + return text + end + for i=0,2000 do + text = text .. tostring(ffi.C.random()) + end + return text +end + +return _M \ No newline at end of file diff --git a/interpreter/luajit/testdata/lua/lualzw.lua b/interpreter/luajit/testdata/lua/lualzw.lua new file mode 100644 index 000000000..936049adf --- /dev/null +++ b/interpreter/luajit/testdata/lua/lualzw.lua @@ -0,0 +1,176 @@ +--[[ +MIT License + +Copyright (c) 2016 Rochet2 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local char = string.char +local type = type +local select = select +local sub = string.sub +local tconcat = table.concat + +local basedictcompress = {} +local basedictdecompress = {} +for i = 0, 255 do + local ic, iic = char(i), char(i, 0) + basedictcompress[ic] = iic + basedictdecompress[iic] = ic +end + +local function dictAddA(str, dict, a, b) + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[str] = char(a,b) + a = a+1 + return dict, a, b +end + +local trace1 = true +local trace2 = true + +local function compress(input) + if type(input) ~= "string" then + return nil, "string expected, got "..type(input) + end + local len = #input + if len <= 1 then + return "u"..input + end + + local dict = {} + local a, b = 0, 1 + + local result = {"c"} + local resultlen = 1 + local n = 2 + local word = "" + for i = 1, len do + local c = sub(input, i, i) + local wc = word..c + if not (basedictcompress[wc] or dict[wc]) then + local write = basedictcompress[word] or dict[word] + if not write then + return nil, "algorithm error, could not fetch word" + end + result[n] = write + resultlen = resultlen + #write + n = n+1 + if len <= resultlen then + return "u"..input + end + dict, a, b = dictAddA(wc, dict, a, b) + word = c + else + word = wc + end + end + result[n] = basedictcompress[word] or dict[word] + resultlen = resultlen+#result[n] + n = n+1 + if len <= resultlen then + return "u"..input + end + if trace1 then + ngx.say(debug.traceback()) + trace1 = false + end + return tconcat(result) +end + +local function dictAddB(str, dict, a, b) + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[char(a,b)] = str + a = a+1 + return dict, a, b +end + +local function decompress(input) + if type(input) ~= "string" then + return nil, "string expected, got "..type(input) + end + + if #input < 1 then + return nil, "invalid input - not a compressed string" + end + + local control = sub(input, 1, 1) + if control == "u" then + return sub(input, 2) + elseif control ~= "c" then + return nil, "invalid input - not a compressed string" + end + input = sub(input, 2) + local len = #input + + if len < 2 then + return nil, "invalid input - not a compressed string" + end + + local dict = {} + local a, b = 0, 1 + + local result = {} + local n = 1 + local last = sub(input, 1, 2) + result[n] = basedictdecompress[last] or dict[last] + n = n+1 + for i = 3, len, 2 do + local code = sub(input, i, i+1) + local lastStr = basedictdecompress[last] or dict[last] + if not lastStr then + return nil, "could not find last from dict. Invalid input?" + end + local toAdd = basedictdecompress[code] or dict[code] + if toAdd then + result[n] = toAdd + n = n+1 + dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b) + else + local tmp = lastStr..sub(lastStr, 1, 1) + result[n] = tmp + n = n+1 + dict, a, b = dictAddB(tmp, dict, a, b) + end + last = code + end + if trace2 then + ngx.say(debug.traceback()) + trace2 = false + end + return tconcat(result) +end + +return { + compress = compress, + decompress = decompress, +} diff --git a/interpreter/luajit/testdata/lua/nested-pcall.lua b/interpreter/luajit/testdata/lua/nested-pcall.lua new file mode 100644 index 000000000..eb0d67ff3 --- /dev/null +++ b/interpreter/luajit/testdata/lua/nested-pcall.lua @@ -0,0 +1,9 @@ +local cjson = require 'cjson.safe' +local d = require '512KB' + +local function call() + local ok,res,err = pcall(cjson.decode,d.data) + pcall(cjson.encode, res) +end + +return {call = call} \ No newline at end of file diff --git a/interpreter/luajit/testdata/lua/sort.lua b/interpreter/luajit/testdata/lua/sort.lua new file mode 100644 index 000000000..0e22af337 --- /dev/null +++ b/interpreter/luajit/testdata/lua/sort.lua @@ -0,0 +1,36 @@ +local _M = {} +local ffi = require "ffi" + +ffi.cdef[[ + long random(); + void qsort(void *base, size_t nel, size_t width, int (*compar)(const long *, const long *)); + int tolower(int); +]] + +function compare(a, b) + -- consume some cpu to get make sure compare gets sampled + for i=0,1000000 do + local x = i * i + end + return a[0] - b[0] +end + +local callback = ffi.cast("int (*)(const long *, const long *)", compare) + +function _M.sort(n) + local arr = ffi.new("long[?]", n) + for i=0,n-1 do + arr[i] = ffi.C.random() + end + ffi.C.qsort(arr, n, ffi.sizeof("long"), callback) + for i=0,n-1 do + print(arr[i]) + end +end + + +if not package.loaded["ngx"] then + _M.sort(20) +end + +return _M diff --git a/interpreter/luajit/testdata/lua/util.lua b/interpreter/luajit/testdata/lua/util.lua new file mode 100644 index 000000000..9f2395756 --- /dev/null +++ b/interpreter/luajit/testdata/lua/util.lua @@ -0,0 +1,9 @@ +local _M = {} +function _M.run_duration(d, f) + local start = ngx.now() + while ngx.now() < start + d do + f() + ngx.update_time() + end +end +return _M diff --git a/interpreter/luajit/testdata/nginx.conf b/interpreter/luajit/testdata/nginx.conf new file mode 100644 index 000000000..db6245766 --- /dev/null +++ b/interpreter/luajit/testdata/nginx.conf @@ -0,0 +1,61 @@ +worker_processes 1; +master_process off; +#daemon off; +events { + worker_connections 1024; +} +http { + lua_package_path "$prefix/lua/?.lua;;"; + server { + listen 8080 reuseport; + location /fib { + default_type text/plain; + content_by_lua_block { + local fib = require "fib" + local u = require "util" + local function f() + ngx.say(fib.calc(32)) + end + u.run_duration(.1, f) + } + } + location /comp { + default_type text/plain; + content_by_lua_block { + local t = require "input-text" + local c = require "comp" + local u = require "util" + local function f() + local text = t.gen() + res = c.comp(text) + ngx.say("Comp: " .. #text .. "->" .. #res) + end + u.run_duration(.1, f) + } + } + location /ffi { + default_type text/plain; + content_by_lua_block { + local q = require "sort" + local u = require "util" + local function f() + local text = q.sort(100) + ngx.say(text) + end + u.run_duration(.1, f) + } + } + location /pcall { + default_type text/plain; + content_by_lua_block { + local u = require "util" + local n = require "nested-pcall" + local function f() + n.call() + end + u.run_duration(.1, f) + ngx.say("pcall") + } + } + } +} \ No newline at end of file diff --git a/interpreter/luajit/trace.go b/interpreter/luajit/trace.go new file mode 100644 index 000000000..084448ca1 --- /dev/null +++ b/interpreter/luajit/trace.go @@ -0,0 +1,105 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +package luajit // import "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" + +import ( + "encoding/binary" + "errors" + "fmt" + "hash/fnv" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe" + "go.opentelemetry.io/ebpf-profiler/remotememory" +) + +// This offset is the same in arm64/x86_64 for all known versions of luajit. +// (gdb) p &((GCtrace*)0)->startins +// $7 = (uint16_t *) 0x50 +const tracePartOffset = 0x50 + +// Definition: +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_jit.h#L423 +type jitStatePart struct { + trace libpf.Address + _ uint32 // freetrace + sizetrace uint32 +} + +// Definition: +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_jit.h#L259 +type trace struct { + _ uint32 /* startins Original bytecode of starting instruction. */ + szmcode uint32 /* Size of machine code. */ + mcode uint64 /* Start of machine code. */ + _ uint32 /* mcloop */ + // For arm is LJ_ABI_PAUTH defined? + // For docker images at least LJ_ABI_PAUTH is not defined. + // ASMFunction mcauth; /* Start of machine code, with ptr auth applied. */ + _ uint16 /* Number of child traces (root trace only). */ + spadjust uint16 /* Stack pointer adjustment (offset in bytes). */ + traceno uint16 /* Trace number. */ + _ uint16 /* Linked trace (or self for loops). */ + root uint16 /* Root trace of side trace (or 0 for root traces). */ +} + +// key == traceId +type traceMap map[uint16]trace + +func getAndHashTraceAddrs(tracesAddr libpf.Address, rm remotememory.RemoteMemory) ( + hash uint64, sizetrace int, traceAddrs []libpf.Address, err error) { + j := jitStatePart{} + if err := rm.Read(tracesAddr, pfunsafe.FromPointer(&j)); err != nil { + return 0, 0, nil, err + } + if j.sizetrace > 65535 { + return 0, 0, nil, fmt.Errorf("invalid sizetrace %d (traces:%x)", j.sizetrace, tracesAddr) + } + traceAddrs = []libpf.Address{} + b := make([]byte, 8) + h := fnv.New64() + binary.LittleEndian.PutUint32(b, j.sizetrace) + _, _ = h.Write(b[:4]) + addrs := make([]libpf.Address, j.sizetrace) + if err := rm.Read(j.trace, pfunsafe.FromSlice(addrs)); err != nil { + return 0, 0, nil, err + } + for _, addr := range addrs { + if addr == 0 { + continue + } + binary.LittleEndian.PutUint64(b, uint64(addr)) + _, _ = h.Write(b) + traceAddrs = append(traceAddrs, addr) + } + return h.Sum64(), int(j.sizetrace), traceAddrs, nil +} + +func loadTraces(tracesAddr libpf.Address, rm remotememory.RemoteMemory) (uint64, traceMap, error) { + h, sztrace, traceAddrs, err := getAndHashTraceAddrs(tracesAddr, rm) + if err != nil { + return 0, nil, err + } + traces := traceMap{} + for _, addr := range traceAddrs { + t := trace{} + if err := rm.Read(addr+tracePartOffset, pfunsafe.FromPointer(&t)); err != nil { + return 0, nil, err + } + if t.traceno > uint16(sztrace) { + return 0, nil, errors.New("invalid traceno") + } + logf("lj: added trace(%d) from %x", t.traceno, tracesAddr) + traces[t.traceno] = t + } + return h, traces, nil +} diff --git a/interpreter/types.go b/interpreter/types.go index 4b2e219c2..ae7d1e4dc 100644 --- a/interpreter/types.go +++ b/interpreter/types.go @@ -39,6 +39,9 @@ var ( UnknownFunctionName = libpf.Intern("") ErrMismatchInterpreterType = errors.New("mismatched interpreter type") + + // Special coredump-only error used to restart ConvertTrace processing + ErrLJRestart = errors.New("lj_restart") ) // The following function Loader and interfaces Data and Instance work together @@ -107,6 +110,10 @@ type EbpfHandler interface { // DeletePidInterpreterMapping removes the element specified by pid, prefix // rom the eBPF map pid_page_to_mapping_info. DeletePidInterpreterMapping(libpf.PID, lpm.Prefix) error + + // CoredumpTest returns whether the unwinder needs special behavior for + // coredump mode to work. + CoredumpTest() bool } // Loader is a function to detect and load data from given interpreter ELF file. diff --git a/libpf/frametype.go b/libpf/frametype.go index 53337a22f..af47bf6cc 100644 --- a/libpf/frametype.go +++ b/libpf/frametype.go @@ -53,6 +53,8 @@ const ( GoFrame FrameType = support.FrameMarkerGo // BEAMFrame identifies the BEAM interpreter frames. BEAMFrame FrameType = support.FrameMarkerBEAM + // LuaJITFrame identifies the LuaJIT interpreter frames. + LuaJITFrame FrameType = support.FrameMarkerLuaJIT ) const ( diff --git a/libpf/frametype_test.go b/libpf/frametype_test.go index 730e56aa6..ef27447b1 100644 --- a/libpf/frametype_test.go +++ b/libpf/frametype_test.go @@ -13,7 +13,7 @@ func TestFrameTypeFromString(t *testing.T) { // Simple check whether all FrameType values can be converted to string and back. for _, ft := range []FrameType{ UnknownFrame, PHPFrame, PythonFrame, NativeFrame, KernelFrame, HotSpotFrame, RubyFrame, - PerlFrame, V8Frame, DotnetFrame} { + PerlFrame, V8Frame, DotnetFrame, LuaJITFrame} { t.Run(ft.String(), func(t *testing.T) { name := ft.String() result := FrameTypeFromString(name) diff --git a/libpf/interpretertype.go b/libpf/interpretertype.go index ff581e117..7ee09a75f 100644 --- a/libpf/interpretertype.go +++ b/libpf/interpretertype.go @@ -31,6 +31,8 @@ const ( V8 InterpreterType = support.FrameMarkerV8 // Dotnet identifies the Dotnet interpreter. Dotnet InterpreterType = support.FrameMarkerDotnet + // LuaJIT identifies the LuaJIT interpreter. + LuaJIT InterpreterType = support.FrameMarkerLuaJIT // Go identifies Go code. Go InterpreterType = support.FrameMarkerGo // BEAM identifies the BEAM interpreter. @@ -73,6 +75,7 @@ var interpreterTypeToString = map[InterpreterType]string{ Dotnet: "dotnet", BEAM: "beam", APMInt: "apm-integration", + LuaJIT: "luajit", Go: "go", GoLabels: "go-labels", } diff --git a/libpf/trace.go b/libpf/trace.go index 172bc8e37..4bca43dcc 100644 --- a/libpf/trace.go +++ b/libpf/trace.go @@ -156,6 +156,10 @@ func (f EbpfFrame) Data() uint64 { return uint64(f[0]) & 0xfffffffffffff } +func (f EbpfFrame) NumVariables() uint8 { + return f.Length() - 1 +} + func (f EbpfFrame) Variable(ndx int) uint64 { return f[ndx+1] } diff --git a/metrics/ids.go b/metrics/ids.go index 76ed4dabe..c59cd49f4 100644 --- a/metrics/ids.go +++ b/metrics/ids.go @@ -650,6 +650,12 @@ const ( // Number of failures to read TLS variables via the DTV IDUnwindErrBadDTVRead = 286 + // Number of attempted LuaJIT unwinds + IDUnwindLuaJITAttempts = 287 + + // Number of times we didn't find an entry for this process in the LuaJIT process info array + IDUnwindLuaJITErrNoProcInfo = 288 + // max number of ID values, keep this as *last entry* - IDMax = 287 + IDMax = 289 ) diff --git a/metrics/metrics.json b/metrics/metrics.json index 1021d7414..b5998f372 100644 --- a/metrics/metrics.json +++ b/metrics/metrics.json @@ -2096,5 +2096,19 @@ "name": "UnwindErrBadDTVRead", "field": "bpf.errors.bad_dtv_read", "id": 286 + }, + { + "description": "Number of attempted LuaJIT unwinds", + "type": "counter", + "name": "UnwindLuaJITAttempts", + "field": "bpf.luajit.attempts", + "id": 287 + }, + { + "description": "Number of times we didn't find an entry for this process in the LuaJIT process info array", + "type": "counter", + "name": "UnwindLuaJITErrNoProcInfo", + "field": "bpf.luajit.errors.no_proc_info", + "id": 288 } ] diff --git a/processmanager/ebpf/ebpf.go b/processmanager/ebpf/ebpf.go index 5bbfbfaab..eb24bc51c 100644 --- a/processmanager/ebpf/ebpf.go +++ b/processmanager/ebpf/ebpf.go @@ -52,6 +52,7 @@ type ebpfMapsImpl struct { BeamProcs *cebpf.Map `name:"beam_procs"` ApmIntProcs *cebpf.Map `name:"apm_int_procs"` GoLabelsProcs *cebpf.Map `name:"go_labels_procs"` + LuajitProcs *cebpf.Map `name:"luajit_procs"` // Stackdelta and process related eBPF maps ExeIDToStackDeltaMaps []*cebpf.Map @@ -127,6 +128,10 @@ func LoadMaps(ctx context.Context, includeTracers types.IncludedTracers, return impl, nil } +func (impl *ebpfMapsImpl) CoredumpTest() bool { + return false +} + // UpdateInterpreterOffsets adds the given moduleRanges to the eBPF map interpreterOffsets. func (impl *ebpfMapsImpl) UpdateInterpreterOffsets(ebpfProgIndex uint16, fileID host.FileID, offsetRanges []util.Range, @@ -168,6 +173,8 @@ func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpreterType) (*ceb return impl.ApmIntProcs, nil case libpf.GoLabels: return impl.GoLabelsProcs, nil + case libpf.LuaJIT: + return impl.LuajitProcs, nil default: return nil, fmt.Errorf("type %d is not (yet) supported", typ) } diff --git a/processmanager/execinfomanager/manager.go b/processmanager/execinfomanager/manager.go index cf6c5364d..0127c53da 100644 --- a/processmanager/execinfomanager/manager.go +++ b/processmanager/execinfomanager/manager.go @@ -20,6 +20,7 @@ import ( golang "go.opentelemetry.io/ebpf-profiler/interpreter/go" "go.opentelemetry.io/ebpf-profiler/interpreter/golabels" "go.opentelemetry.io/ebpf-profiler/interpreter/hotspot" + "go.opentelemetry.io/ebpf-profiler/interpreter/luajit" "go.opentelemetry.io/ebpf-profiler/interpreter/nodev8" "go.opentelemetry.io/ebpf-profiler/interpreter/perl" "go.opentelemetry.io/ebpf-profiler/interpreter/php" @@ -124,7 +125,9 @@ func NewExecutableInfoManager( if includeTracers.Has(types.BEAMTracer) { interpreterLoaders = append(interpreterLoaders, beam.Loader) } - + if includeTracers.Has(types.LuaJITTracer) { + interpreterLoaders = append(interpreterLoaders, luajit.Loader) + } interpreterLoaders = append(interpreterLoaders, apmint.Loader) if includeTracers.Has(types.Labels) { interpreterLoaders = append(interpreterLoaders, golabels.Loader) @@ -225,7 +228,7 @@ func (mgr *ExecutableInfoManager) AddOrIncRef(fileID host.FileID, } // Create the LoaderInfo for interpreter detection - loaderInfo := interpreter.NewLoaderInfo(fileID, elfRef) + loaderInfo := interpreter.NewLoaderInfo(fileID, elfRef, intervalData.Deltas) // Insert a corresponding record into our map. info = &entry{ diff --git a/support/ebpf/errors.h b/support/ebpf/errors.h index 0da717277..b798f5990 100644 --- a/support/ebpf/errors.h +++ b/support/ebpf/errors.h @@ -222,7 +222,22 @@ typedef enum ErrorCode { ERR_BEAM_MODULES_READ_FAILURE = 7005, // BEAM: Ran out of iterations searching for the current code header - ERR_BEAM_RANGE_SEARCH_EXHAUSTED = 7006 + ERR_BEAM_RANGE_SEARCH_EXHAUSTED = 7006, + + // LuaJIT: No entry for this process exists in the LuaJIT process info array + ERR_LUAJIT_NO_PROC_INFO = 7007, + + // LuaJIT: Unable to read the Lua context + ERR_LUAJIT_READ_LUA_CONTEXT = 7008, + + // LuaJIT: Unable to read the Lua frame + ERR_LUAJIT_FRAME_READ = 7009, + + // LuaJIT: context pointer validity check failed + ERR_LUAJIT_L_MISMATCH = 7010, + + // LuaJIT: PC exceeds 24 bits + ERR_LUAJIT_INVALID_PC = 7011 } ErrorCode; #endif // OPTI_ERRORS_H diff --git a/support/ebpf/extmaps.h b/support/ebpf/extmaps.h index 8b3676476..9e0472e53 100644 --- a/support/ebpf/extmaps.h +++ b/support/ebpf/extmaps.h @@ -48,6 +48,7 @@ extern struct ruby_procs_t ruby_procs; extern struct stack_delta_page_to_info_t stack_delta_page_to_info; extern struct unwind_info_array_t unwind_info_array; extern struct v8_procs_t v8_procs; +extern struct luajit_procs_t luajit_procs; #endif // TESTING_COREDUMP #endif // OPTI_EXTMAPS_H diff --git a/support/ebpf/frametypes.h b/support/ebpf/frametypes.h index aae841d98..3d7f90cfc 100644 --- a/support/ebpf/frametypes.h +++ b/support/ebpf/frametypes.h @@ -33,6 +33,8 @@ #define FRAME_MARKER_GO 0xB // Indicates a BEAM frame #define FRAME_MARKER_BEAM 0xC +// Indicates a LuaJIT frame +#define FRAME_MARKER_LUAJIT 0xD // Frame flags // Indicates that this frame is an error frame. diff --git a/support/ebpf/luajit.h b/support/ebpf/luajit.h new file mode 100644 index 000000000..540f2808a --- /dev/null +++ b/support/ebpf/luajit.h @@ -0,0 +1,27 @@ +// Assigned in HA to anonymous executable virtual memory ranges in the nginx process. +#define LUAJIT_JIT_FILE_ID 42 + +// Special value for FFI functions +#define LUAJIT_FFI_FUNC 0xff1 + +// A normal LuaJIT frame +#define LUAJIT_NORMAL_FRAME 0 + +// A fake "frame" that just reports the G pointer. +#define LUAJIT_G_REPORT 0xff2 + +// This is CFRAME_SIZE in src/lj_frame.h +// We could dynamically get this from lj_vm_ffi_callback disassembly and look for: +// lea rax, [rsp+CFRAME_SIZE] +// https://github.com/openresty/luajit2/blob/7952882d/src/vm_x64.dasc#L2725 +#define LUAJIT_CFRAME_SPACE_X86_64 80 +// This is CFRAME_SIZE in src/lj_frame.h +// We could dynamically get this from lj_vm_ffi_callback disassembly and look for the +// add to sp register instruction but that is not available in stripped binaries. +#define LUAJIT_CFRAME_SPACE_AARCH64 208 + +#if defined(__x86_64__) + #define LUAJIT_CFRAME_SPACE LUAJIT_CFRAME_SPACE_X86_64 +#elif defined(__aarch64__) + #define LUAJIT_CFRAME_SPACE LUAJIT_CFRAME_SPACE_AARCH64 +#endif diff --git a/support/ebpf/luajit_tracer.ebpf.c b/support/ebpf/luajit_tracer.ebpf.c new file mode 100644 index 000000000..ad5d99da1 --- /dev/null +++ b/support/ebpf/luajit_tracer.ebpf.c @@ -0,0 +1,723 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +// This file contains the code and map definitions for the Luajit tracer + +#include "bpfdefs.h" +#include "errors.h" +#include "luajit.h" +#include "tracemgmt.h" +#include "types.h" + +struct luajit_procs_t { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, pid_t); + __type(value, LuaJITProcInfo); + __uint(max_entries, 1024); +} luajit_procs SEC(".maps"); + +// The number of LuaJIT frames to unwind per frame-unwinding eBPF program. +#define FRAMES_PER_WALK_LUAJIT_STACK 15 + +// Non error checking bpf read, used sparingly for reading sections of the stack after +// we've established we can read neighboring memory. +#define deref(o) \ + ({ \ + void *__val; \ + bpf_probe_read_user(&__val, sizeof(void *), o); \ + __val; \ + }) + +#define L_PART_OFFSET 0x10 +// (gdb) p/x sizeof(GCproto) +// $4 = 0x68 +#define GCPROTO_SIZE 0x68 + +// This is L offset into interpreter stack frames. +#define L_STACK_OFFSET 0x10 + +///////// BEGIN code copied from luajit2 sources. + +#define LJ_FR2 1 +#define LJ_GCVMASK (((u64)1 << 47) - 1) +enum { + FRAME_LUA, + FRAME_C, + FRAME_CONT, + FRAME_VARG, + FRAME_LUAP, + FRAME_CP, + FRAME_PCALL, + FRAME_PCALLH +}; +#define FRAME_TYPE 3 +#define FRAME_P 4 +#define FRAME_TYPEP (FRAME_TYPE | FRAME_P) + +enum { LJ_CONT_TAILCALL, LJ_CONT_FFI_CALLBACK }; /* Special continuations. */ + +// Use luajit2 style macros in case we come back and want to implement +// support for luajit's compressed 32 bit pointer/value scheme, idea +// being we'd implement all the macros for both systems and build +// two unwinders. Also the macros should make the code look familiar to +// those familiar w/ luajit. +#define bc_a(i) ((u32)(((i) >> 8) & 0xff)) +#define gcval(o) ((void *)((u64)(deref(o)) & LJ_GCVMASK)) +#define frame_gc(f) (gcval((f)-1)) +#define obj2gco(v) ((void *)(v)) +#define frame_type(f) (f & FRAME_TYPE) +#define frame_typep(f) (f & FRAME_TYPEP) +#define frame_islua(f) (frame_type(f) == FRAME_LUA) +#define frame_isvarg(f) (frame_typep(f) == FRAME_VARG) +#define frame_isc(f) (frame_type(f) == FRAME_C) +#define frame_sized(fval) (((s32)fval) & ~FRAME_TYPEP) +#define frame_prevd(f, fval) ((TValue *)((char *)(f)-frame_sized(fval))) +#define frame_func(f) (frame_gc(f)) +#define frame_pc(f) (const u32 *)(f) +#define frame_iscont(f) (frame_typep(f) == FRAME_CONT) +#define frame_contv(f) ((u64)(deref((f)-3))) +#define frame_iscont_fficb(f) (frame_contv(f) == LJ_CONT_FFI_CALLBACK) + +#define restorestack(L, n) ((TValue *)((char *)L.stack + (n))) + +#if defined(__x86_64__) + #define CFRAME_OFS_PREV (4 * 8) + #define CFRAME_OFS_PC (3 * 8) + #define CFRAME_OFS_NRES (2 * 4) + #define CFRAME_OFS_L (2 * 8) +#elif defined(__aarch64__) + #define CFRAME_OFS_PREV 0 + #define CFRAME_OFS_NRES 40 + #define CFRAME_OFS_L 16 + #define CFRAME_OFS_PC 8 +#endif + +#define CFRAME_RESUME 1 +#define CFRAME_UNWIND_FF 2 /* Only used in unwinder. */ +#define CFRAME_RAWMASK (~(s64)(CFRAME_RESUME | CFRAME_UNWIND_FF)) +#define cframe_nres_addr(cf) (s32 *)(((char *)(cf)) + CFRAME_OFS_NRES) +#define cframe_raw(cf) ((void *)((s64)(cf) & CFRAME_RAWMASK)) +#define cframe_pc_addr(cf) (void *)(((char *)(cf)) + CFRAME_OFS_PC) +#define cframe_L_addr(cf) (void *)(((char *)(cf)) + CFRAME_OFS_L) +#define cframe_prev(cf) deref((void **)(((char *)(cf)) + CFRAME_OFS_PREV)) + +/* Invalid bytecode position. */ +#define NO_BCPOS (~(u32)0) +#define FF_LUA 0 + +///////// END code copied from luajit2 sources. + +static EBPF_INLINE TValue *frame_prevl(TValue *f, TValue frame_val) +{ + // This is the EBPF version of the frame_prevl macro. + // #define frame_prevl(f) ((f) - (1+LJ_FR2+bc_a(frame_pc(f)[-1]))) + int delta = 1 + LJ_FR2; + u32 prevIns; + bpf_probe_read_user(&prevIns, sizeof(u32), (u32 *)(frame_val)-1); + delta += bc_a(prevIns); + return f - delta; +} + +// lj_debug_framepc for a function. There's no easy way to look at this, basically +// there's a bunch of places the return address is stored depending on the frame +// type. +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_debug.c#L53 +static EBPF_INLINE ErrorCode lj_debug_framepc( + struct pt_regs *ctx, PerCPURecord *record, void *fn, u32 *startpc, TValue *prevframe, u32 *pc) +{ + LJFuncPart *func = &record->luajitUnwindScratch.f; + if (bpf_probe_read_user(func, sizeof(LJFuncPart), (void **)fn + 1)) { + return ERR_LUAJIT_FRAME_READ; + } + if (func->ffid != FF_LUA) { /* Cannot derive a PC for non-Lua functions. */ + DEBUG_PRINT("lj: non-lua function %lx", (unsigned long)func->ffid); + *pc = NO_BCPOS; + return ERR_OK; + } + const u32 *ins = NULL; + if (prevframe == NULL) { /* Lua function on top. */ + bool leaf_in_lua = (record->initialUnwinder == PROG_UNWIND_LUAJIT); + DEBUG_PRINT("lj: leaf_in_lua: %d", leaf_in_lua); + if (leaf_in_lua) { +#if defined(__x86_64__) + ins = (u32 *)ctx->bx; +#elif defined(__aarch64__) + ins = (u32 *)ctx->regs[21]; +#else + #error unsupported architecture +#endif + } else { + void *cf = cframe_raw(record->luajitUnwindScratch.L.cframe); + if (cf == NULL) { + DEBUG_PRINT("lj: cframe null"); + *pc = NO_BCPOS; + return ERR_OK; + } + void *pc_addr = cframe_pc_addr(cf); + void *L_addr = cframe_L_addr(cf); + void *L_ptr; + if (bpf_probe_read_user(&ins, sizeof(void *), pc_addr)) { + DEBUG_PRINT("lj: pc_addr read failed"); + return ERR_LUAJIT_FRAME_READ; + } + if (bpf_probe_read_user(&L_ptr, sizeof(void *), L_addr)) { + DEBUG_PRINT("lj: L_addr read failed"); + return ERR_LUAJIT_FRAME_READ; + } + if (ins == (void *)record->luajitUnwindState.L_ptr || ins == NULL) { + DEBUG_PRINT("lj: ins == L or NULL"); + *pc = NO_BCPOS; + return ERR_OK; + } + } + } else { + TValue frame_val; + if (bpf_probe_read_user(&frame_val, sizeof(void *), prevframe)) { + DEBUG_PRINT("lj: frame_val 1 read failed"); + return ERR_LUAJIT_FRAME_READ; + } + if (frame_islua(frame_val)) { + ins = frame_pc(frame_val); + } else if (frame_iscont(frame_val)) { + // ins = frame_contpc(nextframe); + if (bpf_probe_read_user(&frame_val, sizeof(void *), prevframe - 2)) { + DEBUG_PRINT("lj: frame_val 3 read failed"); + return ERR_LUAJIT_FRAME_READ; + } + ins = frame_pc(frame_val); + } else { + /* Lua function below errfunc/gc/hook: find cframe to get the PC. */ + DEBUG_PRINT("lj: lua function below errfunc/gc/hook"); + // This code is commented out because we haven't figured out how to test it. + // void *cf = cframe_raw(record->luajitUnwindScratch.L.cframe); + // TValue *f = record->luajitUnwindScratch.L.base-1; + // #define CFRAME_SEARCH_LOOPS 5 + // #define CFRAME_SEARCH_LOOPS2 5 + + // #pragma unroll + // for (int i = 0; i < CFRAME_SEARCH_LOOPS; i++) { + // if (cf == NULL) { + // *pc = NO_BCPOS; + // return ERR_OK; + // } + // #pragma unroll + // for (int j = 0; j < CFRAME_SEARCH_LOOPS2; j++) { + // s32 *nresp = cframe_nres_addr(cf); + // s32 nres; + // bpf_probe_read_user(&nres, sizeof(s32), nresp); + // if (f >= restorestack(record->luajitUnwindScratch.L, -nres)) + // break; + // cf = cframe_raw(cframe_prev(cf)); + // if (cf == NULL) { + // *pc = NO_BCPOS; + // return ERR_OK; + // } + // } + // if (f < prevframe) + // break; + // if (bpf_probe_read_user(&frame_val, sizeof(void*), prevframe)) { + // DEBUG_PRINT("lj: frame_val 4 read failed"); + // return ERR_LUAJIT_FRAME_READ; + // } + // if (frame_islua(frame_val)) { + // f = frame_prevl(f, frame_val); + // } else { + // if (frame_isc(frame_val) || (frame_iscont(frame_val) && frame_iscont_fficb(f))) + // cf = cframe_raw(cframe_prev(cf)); + // f = frame_prevd(f,frame_val); + // } + // } + // const u32 **insp = cframe_pc_addr(cf); + // if (bpf_probe_read_user(&ins, sizeof(void*), insp)) { + // DEBUG_PRINT("lj: ins read failed"); + // return ERR_LUAJIT_FRAME_READ; + // } + if (!ins) { + *pc = NO_BCPOS; + return ERR_OK; + } + } + } + // startpc can be for a different function if we land on instructions where things aren't synced. + // For instance the PC is up to date on the stack but jit_base wasn't updated yet. + DEBUG_PRINT("lj: ins: %llx, startpc: %llx", (u64)ins, (u64)startpc); + if (ins < startpc) { + DEBUG_PRINT("lj: ins < startpc, setting *pc = NO_BCPOS"); + *pc = NO_BCPOS; + return ERR_OK; + } + *pc = ins - startpc - 1; + DEBUG_PRINT("ins, startpc good: setting *pc = %llx", (u64)*pc); + return ERR_OK; +} + +// For Lua we need the caller and callee to process a frame. +// The callee_pt is a pointer to the GCproto of the function being called, the +// callee_pc is an index into its bytecode. The caller_pt is the +// GCproto of the calling function and the caller_pc is the index into its +// bytecode which we will walk backwards in userland to figure out a name for the +// callee. The callee_pc is for information purposes only, so the user can see where +// execution was. +static EBPF_INLINE ErrorCode lj_push_frame( + UnwindState *state, Trace *trace, u64 callee_pt, u64 caller_pt, u32 callee_pc, u32 caller_pc) +{ + u64 *data = + push_frame(state, trace, FRAME_MARKER_LUAJIT, FRAME_FLAG_PID_SPECIFIC, LUAJIT_NORMAL_FRAME, 3); + if (!data) + return ERR_STACK_LENGTH_EXCEEDED; + data[0] = callee_pt; + data[1] = caller_pt; + data[2] = ((u64)callee_pc << 32) | caller_pc; + return ERR_OK; +} + +static EBPF_INLINE ErrorCode lj_record_frame( + struct pt_regs *ctx, PerCPURecord *record, TValue *frame, TValue frame_value, TValue *prevframe) +{ + LJScratchSpace *scr = &record->luajitUnwindScratch; + if (frame_isvarg(frame_value)) { + DEBUG_PRINT("lj: vararg frame"); + return ERR_OK; /* Skip vararg frames. */ + } + if (frame_gc(frame) == obj2gco(record->luajitUnwindState.L_ptr)) { + DEBUG_PRINT("lj: skip dummy frame"); + return ERR_OK; /* Skip dummy frames. See lj_err_optype_call(). */ + } + void *fn = frame_func(frame); + LJFuncPart *f = &scr->f; + // +1 to skip the 8 byte GCHeader + if (bpf_probe_read_user(f, sizeof(LJFuncPart), (void **)fn + 1)) { + return ERR_LUAJIT_FRAME_READ; + } + + if (f->ffid != FF_LUA) { + DEBUG_PRINT("lj: lj_record_frame: ffi function %lx", (unsigned long)f->ffid); + // We can't derive a name for this function, so we'll just emit a pseudo frame. + // XXX[btv] where does this get read? + u64 *data = push_frame( + &record->state, + &record->trace, + FRAME_MARKER_LUAJIT, + FRAME_FLAG_PID_SPECIFIC, + LUAJIT_FFI_FUNC, + 1); + if (!data) + return ERR_STACK_LENGTH_EXCEEDED; + data[0] = frame_value; + } + + u32 *start_ip = (u32 *)f->pc; + // The bytecode is allocated after the GCproto. + void *proto = (char *)f->pc - GCPROTO_SIZE; + + u32 pc; + ErrorCode err = lj_debug_framepc(ctx, record, fn, start_ip, prevframe, &pc); + if (err) { + DEBUG_PRINT("lj: lj_debug_framepc err %u", err); + return err; + } + if (pc == NO_BCPOS) { + DEBUG_PRINT("lj: no bcpos"); + pc = 0xffffff; + } + // Top frame, we can't emit anything yet because we don't know the caller PC but stash callee_pc + // for next time. + if (record->luajitUnwindState.prevframe == NULL) { + goto exit; + } + + DEBUG_PRINT("lj: record frame callee %lx:%u", (unsigned long)scr->prev_proto, scr->prev_pc); + DEBUG_PRINT("lj: record frame caller %lx:%u", (unsigned long)proto, pc); + err = lj_push_frame( + &record->state, &record->trace, (u64)scr->prev_proto, (u64)proto, scr->prev_pc, pc); +exit: + scr->prev_proto = proto; + scr->prev_pc = pc; + return err; +} + +// See: +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_frame.h#L33 +static EBPF_INLINE ErrorCode lj_prev_frame(PerCPURecord *record, TValue frame_val) +{ + TValue *frame = record->luajitUnwindState.frame; + if (frame_islua(frame_val)) { + frame = frame_prevl(frame, frame_val); + } else { + frame = frame_prevd(frame, frame_val); + } + if (bpf_probe_read_user(&frame_val, sizeof(TValue), frame)) { + return ERR_LUAJIT_FRAME_READ; + } + if (frame_isvarg(frame_val)) { + frame = frame_prevd(frame, frame_val); + } + record->luajitUnwindState.frame = frame; + return ERR_OK; +} + +// Unwind a frame of native code; for example, +// a CFRAME at the C/Lua boundary. +// +// `is_jit`should be true if there is JITted code anywhere in the Lua code corresponding to this +// cframe. +static EBPF_INLINE ErrorCode +unwind_native_frame(const LuaJITProcInfo *info, UnwindState *state, bool is_jit) +{ + /* Interpreter frames unwind naturally, we need to poke sp/pc for JIT frames */ + /* so we need to call this for the native unwinder to continue over them. */ + /* https://github.com/openresty/luajit2/blob/7952882d/src/lj_frame.h#L178 */ + u32 spadjust; + if (is_jit) { + spadjust = (u32)state->text_section_id; + if (spadjust == 0) { + // Guess the default. + spadjust = info->cframe_size_jit; + } + } else { + spadjust = LUAJIT_CFRAME_SPACE; + } + + state->sp += spadjust; + u64 frame[2]; + if (bpf_probe_read_user(frame, sizeof(frame), (void *)(state->sp - sizeof(frame)))) { + DEBUG_PRINT("lj: failed to read frame"); + increment_metric(metricID_UnwindLuaJITErrNoContext); + return ERR_LUAJIT_READ_LUA_CONTEXT; + } + + state->fp = frame[0]; + u64 pc = state->pc; + (void)pc; // appease non-debug builds + state->pc = frame[1]; + state->return_address = true; + DEBUG_PRINT( + "lj: unwound frame old pc:(%lx) to new pc:%lx, sp:%lx", + (unsigned long)pc, + (unsigned long)state->pc, + (unsigned long)state->sp); + + return ERR_OK; +} + +// walk_luajit_stack walks the luajit stack by inspecting the frame values +// and finding ones that indicate a function call frame. Code inspired by +// lj_debug_frame. +// https://github.com/openresty/luajit2/blob/7952882d/src/lj_debug.c#L25 +static EBPF_INLINE ErrorCode walk_luajit_stack( + struct pt_regs *ctx, PerCPURecord *record, const LuaJITProcInfo *info, int *next_unwinder) +{ + bool exitToNative = false; + ErrorCode err; + LJState *L = &record->luajitUnwindScratch.L; + TValue *prevframe = record->luajitUnwindState.prevframe; + + for (int i = 0; i < FRAMES_PER_WALK_LUAJIT_STACK; i++) { + // A LuaJIT stack segment looks like this, where each cell is a TValue: + // [ FUNC | PC | ARG1 | ARG2 | ARG3 | ..... ] + // ^ ^ + // | | + // | BASE + // | + // frame + // + // In this diagram, `frame` points to our frame pointer (set below), + // and BASE points to the bottom of the stack frame that is exposed to user code + // (which can't access FUNC and PC). + // Every time Lua calls into C, it sets L->base appropriately and then never + // lets the C function read below it, so + // it effectively has its own isolated stack. But in reality, + // from the interpreter's perspective, these segments are concatenated into one array + // pointed to by the L->stack object. + // + // For reasons of not-very-interesting internal implementation details, + // BASE must always be two elements above the bottom of the stack, + // even when the stack is logically empty. So whenever a new Lua state is created + // (e.g. via luaL_newstate() or lua_newthread()), the interpreter + // pushes two dummy values (see + // https://github.com/luajit/luajit/blob/659a6169/src/lj_state.c#L168-L180). + // + // Thus, when `diff` (set below) is <= 2, we've actually unwound past the logical + // root of the stack, which should never happen... + TValue *frame = (TValue *)(record->luajitUnwindState.frame); + + int diff = frame - L->stack; + DEBUG_PRINT("lj: distance to bot: %d", diff); + + if (diff <= 2) { + // Need to clear 'frame' if we have more than one LuaJIT call on the stack, + // ie two different instances of LuaJIT, not sure if this happens in practice. + // While conceptually this makes sense its kind of an edge case and + // if we clear it we run into a situation where if we clear it and + // encounter another luajit interpreter frame we'll walk the same stack + // twice. This occurs in currently unsupported unhandled FFI callback use + // cases where we need to jump back to the native unwinder, the code below + // that does this is probably correct but its untested because we don't + // properly unwind LuaJIT FFI frames (which is a different kind of JIT). + // When that's fixed we can uncomment this and be more correct. + // record->luajitUnwindState.frame = NULL; + + DEBUG_PRINT("lj: unwound past the end of the stack... this shouldn't happen"); + + // Let's try to continue anyway, to match the old behavior. + + // We have processed all frames, send final frame which will just have + // a callee proto/pc and no caller proto/pc. This is fine, we'll make one + // up, e.g. "main". + LJScratchSpace *scr = &record->luajitUnwindScratch; + if ((err = lj_push_frame( + &record->state, &record->trace, (u64)scr->prev_proto, (u64)0, scr->prev_pc, 0))) { + return err; + } + if (record->luajitUnwindState.is_jit) { + unwind_native_frame(info, &record->state, true); + + if ((err = resolve_unwind_mapping(record, next_unwinder)) != ERR_OK) { + DEBUG_PRINT("lj: failed to walk over jit frame"); + *next_unwinder = PROG_UNWIND_STOP; + return err; + } + } + DEBUG_PRINT("lj: end lua frame"); + *next_unwinder = PROG_UNWIND_NATIVE; + return ERR_OK; + } + + TValue frame_val; + if (bpf_probe_read_user(&frame_val, sizeof(TValue), frame)) { + return ERR_LUAJIT_FRAME_READ; + } + + // If we have a frame with its own C stack frame we need to exit to native unwinder. + // In addition, if this is the rootmost C stack, we are done with Lua entirely. + bool done_with_lua = false; + if (frame_typep(frame_val) == FRAME_CP) { + void *cf = record->luajitUnwindState.cframe; + if (cf == NULL) { + cf = record->luajitUnwindState.cframe = record->luajitUnwindScratch.L.cframe; + } + if (cf != NULL) { + void *prev = cframe_prev(cframe_raw(cf)); + done_with_lua = !prev; + + unwind_native_frame( + info, &record->state, ((u32)(record->state.text_section_id >> 32)) == LUAJIT_JIT_FILE_ID); + if ((err = resolve_unwind_mapping(record, next_unwinder)) != ERR_OK) { + *next_unwinder = PROG_UNWIND_STOP; + return err; + } + DEBUG_PRINT( + "lj: walk_lua_stack: cframe encountered, leaving unwinder, %lx prev: %lx", + (unsigned long)cf, + (unsigned long)prev); + record->luajitUnwindState.cframe = prev; + *next_unwinder = PROG_UNWIND_NATIVE; + + exitToNative = true; + } + } + if ((err = lj_record_frame(ctx, record, frame, frame_val, prevframe))) { + DEBUG_PRINT("lj: walk_lua_stack: lj_record_frame=%d", err); + return err; + } + if ((frame_iscont(frame_val) && frame_iscont_fficb(frame))) { + // If we have a callback from C into Lua switch to native unwinder. + // TODO: should we do the same for cpcall frames? + DEBUG_PRINT("lj: walk_lua_stack: continuation callback frame %lx", (unsigned long)frame_val); + // We want to record next Lua frame then exit to native. + exitToNative = true; + } + record->luajitUnwindState.prevframe = prevframe = frame; + if ((err = lj_prev_frame(record, frame_val))) { + return err; + } + if (exitToNative) { + // Let the native walker kick in now when we called into lua from C. + *next_unwinder = PROG_UNWIND_NATIVE; + if (done_with_lua) { + // We have processed all frames, send final frame which will just have + // a callee proto/pc and no caller proto/pc. This is fine, we'll make one + // up, e.g. "main". + LJScratchSpace *scr = &record->luajitUnwindScratch; + if ((err = lj_push_frame( + &record->state, &record->trace, (u64)scr->prev_proto, (u64)0, scr->prev_pc, 0))) { + return err; + } + } + + return ERR_OK; + } + } + + // We exhausted loops, come back for more! + *next_unwinder = PROG_UNWIND_LUAJIT; + + return ERR_OK; +} + +static EBPF_INLINE ErrorCode +find_context(struct pt_regs *ctx, PerCPURecord *record, const LuaJITProcInfo *info) +{ + bool reportG = false; + void *G_ptr = NULL; + void *L_ptr; + UnwindState *state = &record->state; + u32 high = (u32)(state->text_section_id >> 32); + + // The initial state is for the entire anonymous/executable memory range to be mapped to + // our unwinder with a token file ID. Then we fire a pid event which will call SynchronizeMappings + // in the HA which will overlay the big anonymous/executable memory range with the actual mappings + // for each trace with a stack adjustment stored in the low bits. + if (high == LUAJIT_JIT_FILE_ID) { + record->luajitUnwindState.is_jit = true; + + // Once the HA fills in text_section_bias with G we'll stop sending these report_pids. + if (state->text_section_bias == 0) { + DEBUG_PRINT("lj: unwinding unmapped JIT frame"); + u64 pid_tgid = (u64)record->trace.pid << 32 | record->trace.tid; + report_pid(ctx, pid_tgid, RATELIMIT_ACTION_DEFAULT); + + // If top frame isn't luajit we can't rely on the register still holding the DISPATCH table, + // but once we propagate G to the HA text_section_bias will be set to the G pointer and we can + // pull cur_L from that. So this is just a bootstrap crutch that just has to work once (or + // never because G also gets picked up from interpreter hits). +#if defined(__x86_64__) + G_ptr = (char *)state->r14 - info->g2dispatch; +#elif defined(__aarch64__) + G_ptr = (char *)state->r22; +#endif + reportG = true; + } else { + G_ptr = (void *)state->text_section_bias; + DEBUG_PRINT("lj: unwinding trace mapped JIT frame %lx", (unsigned long)G_ptr); + } + if (bpf_probe_read_user(&L_ptr, sizeof(void *), (void *)(G_ptr + info->cur_L_offset))) { + DEBUG_PRINT( + "lj: failed to read G->cur_L %lx", (unsigned long)((void *)(G_ptr + info->cur_L_offset))); + increment_metric(metricID_UnwindLuaJITErrNoContext); + return ERR_LUAJIT_READ_LUA_CONTEXT; + } + } else { + // Interpreter, L is always [rsp+0x10]. + if (bpf_probe_read_user(&L_ptr, sizeof(void *), (void *)(state->sp + L_STACK_OFFSET))) { + DEBUG_PRINT("lj: failed to read stack"); + increment_metric(metricID_UnwindLuaJITErrNoContext); + return ERR_LUAJIT_READ_LUA_CONTEXT; + } + reportG = true; + } + + LJScratchSpace *scr = &record->luajitUnwindScratch; + if (bpf_probe_read_user(&scr->L, sizeof(LJState), (char *)L_ptr + L_PART_OFFSET)) { + DEBUG_PRINT("lj: bad L: failed to read L from: %lx", (unsigned long)L_ptr); + increment_metric(metricID_UnwindLuaJITErrNoContext); + return ERR_LUAJIT_READ_LUA_CONTEXT; + } + + // If we came through interpreter we won't have G yet. + if (G_ptr == NULL) { + G_ptr = (void *)scr->L.glref; + } + + if (bpf_probe_read_user( + &scr->G, sizeof(LJGlobalPart), (void *)((char *)G_ptr + info->cur_L_offset))) { + DEBUG_PRINT( + "lj: bad G picked up from L: failed to read G->cur_L: %lx, %lx", + (unsigned long)G_ptr, + (unsigned long)info->cur_L_offset); + increment_metric(metricID_UnwindLuaJITErrNoContext); + return ERR_LUAJIT_READ_LUA_CONTEXT; + } + + if (L_ptr != scr->G.cur_L) { + DEBUG_PRINT( + "lj: L context check failed: %lx != %lx", (unsigned long)L_ptr, (unsigned long)scr->G.cur_L); + increment_metric(metricID_UnwindLuaJITErrLMismatch); + return ERR_LUAJIT_L_MISMATCH; + } + + DEBUG_PRINT("lj: L context: %lx", (unsigned long)L_ptr); + record->luajitUnwindState.L_ptr = L_ptr; + + // If we have valid context let's report it if we haven't mapped its traces yet. + if (reportG) { + u64 *data = push_frame( + &record->state, + &record->trace, + FRAME_MARKER_LUAJIT, + FRAME_FLAG_PID_SPECIFIC, + LUAJIT_G_REPORT, + 1); + if (!data) + return ERR_STACK_LENGTH_EXCEEDED; + data[0] = (u64)G_ptr; + } + + // The JIT doesn't update base as it goes but it does update G.jit_base. + if (high == LUAJIT_JIT_FILE_ID) { + record->luajitUnwindState.frame = scr->G.jit_base - 1; + } + // otherwise, if the first unwinder was Luajit, then we're in + // the interpreter. L->base won't have been updated, but + // we should have base in a register. + // + // From vm_x64.dasc: + // |.define BASE, rdx + else if (record->initialUnwinder == PROG_UNWIND_LUAJIT) { +#if defined(__x86_64__) + record->luajitUnwindState.frame = (TValue *)(ctx->dx) - 1; +#elif defined(__aarch64__) + record->luajitUnwindState.frame = (TValue *)(ctx->regs[19]) - 1; +#else + #error unsupported architecture +#endif + } else { + record->luajitUnwindState.frame = scr->L.base - 1; + } + + return ERR_OK; +} + +static EBPF_INLINE int unwind_luajit(struct pt_regs *ctx) +{ + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + UnwindState *state = &record->state; + int unwinder = get_next_unwinder_after_interpreter(); + ErrorCode error = ERR_OK; + u32 pid = record->trace.pid; + LuaJITProcInfo *info = bpf_map_lookup_elem(&luajit_procs, &pid); + if (!info) { + DEBUG_PRINT("lj: no LuaJIT introspection data"); + error = ERR_LUAJIT_NO_PROC_INFO; + increment_metric(metricID_UnwindLuaJITErrNoProcInfo); + goto exit; + } + increment_metric(metricID_UnwindLuaJITAttempts); + + if (record->luajitUnwindState.frame == 0) { + if ((error = find_context(ctx, record, info))) { + goto exit; + } + } + + if ((error = walk_luajit_stack(ctx, record, info, &unwinder))) { + goto exit; + } + +exit: + state->unwind_error = error; + tail_call(ctx, unwinder); + return -1; +} +MULTI_USE_FUNC(unwind_luajit) diff --git a/support/ebpf/native_stack_trace.ebpf.c b/support/ebpf/native_stack_trace.ebpf.c index dde9ec8af..d281d4d24 100644 --- a/support/ebpf/native_stack_trace.ebpf.c +++ b/support/ebpf/native_stack_trace.ebpf.c @@ -457,6 +457,7 @@ static EBPF_INLINE ErrorCode unwind_one_frame(PerCPURecord *record, bool *stop) state->fp = rt_regs[29]; state->lr = normalize_pac_ptr(rt_regs[30]); state->r20 = rt_regs[20]; + state->r7 = rt_regs[7]; state->r22 = rt_regs[22]; state->r28 = rt_regs[28]; @@ -493,7 +494,8 @@ static EBPF_INLINE ErrorCode unwind_one_frame(PerCPURecord *record, bool *stop) // Resolve the frame CFA (previous PC is fixed to CFA) address state->cfa = unwind_calc_register(state, info->baseReg, param); - + DEBUG_PRINT("prev cfa: %llx", state->cfa); + // Resolve Return Address, it is either the value of link register or // stack address where RA is stored u64 ra = unwind_calc_register(state, info->auxBaseReg, info->auxParam); @@ -517,7 +519,7 @@ static EBPF_INLINE ErrorCode unwind_one_frame(PerCPURecord *record, bool *stop) return ERR_NATIVE_LR_UNWINDING_MID_TRACE; } } else { - DEBUG_PRINT("RA: %016llX", (u64)ra); + DEBUG_PRINT("RA: *(%016llX)", (u64)ra); // read the value of RA from stack int err; @@ -536,6 +538,7 @@ static EBPF_INLINE ErrorCode unwind_one_frame(PerCPURecord *record, bool *stop) } state->pc = normalize_pac_ptr(ra); state->sp = state->cfa; + DEBUG_PRINT("fp, ra, sp: %llx, %llx, %llx", state->fp, ra, state->sp); unwinder_mark_nonleaf_frame(state); frame_ok: increment_metric(metricID_UnwindNativeFrames); diff --git a/support/ebpf/tracemgmt.h b/support/ebpf/tracemgmt.h index 8ac15130b..655a082f3 100644 --- a/support/ebpf/tracemgmt.h +++ b/support/ebpf/tracemgmt.h @@ -238,6 +238,11 @@ static inline EBPF_INLINE PerCPURecord *get_pristine_per_cpu_record() record->rubyUnwindState.stack_ptr = 0; record->rubyUnwindState.last_stack_frame = 0; record->rubyUnwindState.cfunc_saved_frame = 0; + record->luajitUnwindState.frame = 0; + record->luajitUnwindState.prevframe = 0; + record->luajitUnwindState.L_ptr = 0; + record->luajitUnwindState.cframe = 0; + record->luajitUnwindState.is_jit = false; record->unwindersDone = 0; record->tailCalls = 0; record->ratelimitAction = RATELIMIT_ACTION_DEFAULT; @@ -330,6 +335,7 @@ static inline EBPF_INLINE u64 frame_header(u8 frame_type, u8 flags, u8 length, u static inline EBPF_INLINE u64 *push_frame( UnwindState *state, Trace *trace, u8 frame_type, u8 frame_flags, u64 frame_data, u8 frame_varlen) { + DEBUG_PRINT("push_frame type: %d flags: %x data: %llx", frame_type, frame_flags, frame_data); const int max_frame_size = sizeof trace->frame_data / sizeof trace->frame_data[0]; const int error_frame_size = 1; @@ -613,6 +619,7 @@ copy_state_regs(UnwindState *state, struct pt_regs *regs, bool interrupted_kerne state->r9 = regs->r9; state->r11 = regs->r11; state->r13 = regs->r13; + state->r14 = regs->r14; state->r15 = regs->r15; // Treat syscalls as return addresses, but not IRQ handling, page faults, etc.. @@ -630,6 +637,7 @@ copy_state_regs(UnwindState *state, struct pt_regs *regs, bool interrupted_kerne state->fp = regs->regs[29]; state->lr = normalize_pac_ptr(regs->regs[30]); state->r20 = regs->regs[20]; + state->r7 = regs->regs[7]; state->r22 = regs->regs[22]; state->r28 = regs->regs[28]; @@ -783,7 +791,8 @@ static inline EBPF_INLINE int collect_trace( } return 0; } - error = get_next_unwinder_after_native_frame(record, &unwinder); + error = get_next_unwinder_after_native_frame(record, &unwinder); + record->initialUnwinder = unwinder; exit: record->state.unwind_error = error; diff --git a/support/ebpf/tracer.ebpf.amd64 b/support/ebpf/tracer.ebpf.amd64 index cda39d181..2d95e91b3 100644 Binary files a/support/ebpf/tracer.ebpf.amd64 and b/support/ebpf/tracer.ebpf.amd64 differ diff --git a/support/ebpf/tracer.ebpf.arm64 b/support/ebpf/tracer.ebpf.arm64 index 512510e3a..8e61121fa 100644 Binary files a/support/ebpf/tracer.ebpf.arm64 and b/support/ebpf/tracer.ebpf.arm64 differ diff --git a/support/ebpf/types.h b/support/ebpf/types.h index 574739e0a..462b22ee1 100644 --- a/support/ebpf/types.h +++ b/support/ebpf/types.h @@ -301,6 +301,18 @@ enum { // number of failures to unwind code object due to its large size metricID_UnwindDotnetErrCodeTooLarge, + // number of attempts to unwind LuaJIT + metricID_UnwindLuaJITAttempts, + + // number of failures to read LuaJIT proc info + metricID_UnwindLuaJITErrNoProcInfo, + + // number of failures to read LuaJIT context pointer + metricID_UnwindLuaJITErrNoContext, + + // number of failures in context pointer validity check + metricID_UnwindLuaJITErrLMismatch, + // number of attempts to read Go custom labels metricID_UnwindGoLabelsAttempts, @@ -358,6 +370,7 @@ typedef enum TracePrograms { PROG_UNWIND_DOTNET10, PROG_GO_LABELS, PROG_UNWIND_BEAM, + PROG_UNWIND_LUAJIT, NUM_TRACER_PROGS, } TracePrograms; @@ -543,6 +556,12 @@ typedef struct BEAMProcInfo { u8 ranges_sizeof; } BEAMProcInfo; +typedef struct LuaJITProcInfo { + u16 g2dispatch; + u16 cur_L_offset; + u16 cframe_size_jit; +} LuaJITProcInfo; + // COMM_LEN defines the maximum length we will receive for the comm of a task. #define COMM_LEN 16 @@ -663,9 +682,9 @@ typedef struct UnwindState { // The per-CPU registers which are not unwound, but needed to be accessed // on leaf frames. #if defined(__x86_64__) - u64 rax, r9, r11, r13, r15; + u64 rax, r9, r11, r13, r14, r15; #elif defined(__aarch64__) - u64 r20, r22, r28; + u64 r7, r20, r22, r28; #endif }; }; @@ -736,6 +755,59 @@ typedef struct RubyUnwindState { u64 cfunc_saved_frame; } RubyUnwindState; +typedef u64 TValue; + +// This layout hasn't changed over LuaJIT versions. +typedef struct LJState { + void *glref; + void *dummy3; + TValue *base; /* Base of currently executing function. */ + TValue *top; /* First free slot in the stack. */ + TValue *maxstack; /* Last free slot in the stack. */ + TValue *stack; /* Stack base. */ + void *openupval; /* List of open upvalues in the stack. */ + void *env; /* Thread environment (table of globals). */ + void *cframe; /* End of C stack frame chain. */ +} LJState; + +// These two are always adjacent, cur_L offset comes from HA. +typedef struct LJGlobalPart { + void *cur_L; + TValue *jit_base; +} LJGlobalPart; + +// Part of a function we need access to, skips first 8 bytes. Again +// this layout (from GCfuncL type) hasn't changed in the history of openresty. +typedef struct LJFuncPart { + u8 marked; + u8 gct; + u8 ffid; + u8 nupvalues; + u32 dummy; + void *env; + void *gclist; + void *pc; // BCIns* to end of GCproto (i.e. startpc) +} LJFuncPart; + +typedef struct LJScratchSpace { + LJState L; + LJGlobalPart G; + LJFuncPart f; + void *G_to_report; + u32 *prev_proto; + u32 prev_pc; +} LJScratchSpace; + +typedef struct LJUnwindState { + TValue *frame; + TValue *prevframe; + void *L_ptr; + // If we have intertwined interpreter and native frames use cframe to track we have more + // jumps back to native unwinder to do. + void *cframe; + bool is_jit; +} LJUnwindState; + // Container for additional scratch space needed by the HotSpot unwinder. typedef struct DotnetUnwindScratchSpace { // Buffer to read nibble map to locate code start. One map entry allows seeking backwards @@ -820,6 +892,8 @@ typedef struct PerCPURecord { PHPUnwindState phpUnwindState; // The current Ruby unwinder state. RubyUnwindState rubyUnwindState; + // The current LuaJIT unwinder state. + LJUnwindState luajitUnwindState; // State for Go and Native custom labels CustomLabelsState customLabelsState; union { @@ -831,6 +905,8 @@ typedef struct PerCPURecord { V8UnwindScratchSpace v8UnwindScratch; // Scratch space for the Python unwinder PythonUnwindScratchSpace pythonUnwindScratch; + // Scratch space for the LuaJIT unwinder + LJScratchSpace luajitUnwindScratch; // Go labels scratch GoMapBucket goMapBucket; // Scratch for Go 1.24 labels @@ -851,6 +927,8 @@ typedef struct PerCPURecord { // ratelimitAction determines the PID event rate limiting mode u8 ratelimitAction; + + int initialUnwinder; } PerCPURecord; // https://github.com/torvalds/linux/blob/e9a6fb0bcdd7609be6969112f3fbfcce3b1d4a7c/include/linux/percpu.h#L24C39-L24C47 diff --git a/support/types.go b/support/types.go index 004e1ab8b..38da57a84 100644 --- a/support/types.go +++ b/support/types.go @@ -22,6 +22,7 @@ const ( FrameMarkerPerl = 0x7 FrameMarkerV8 = 0x8 FrameMarkerDotnet = 0xa + FrameMarkerLuaJIT = 0xd FrameMarkerBEAM = 0xc FrameMarkerGo = 0xb ) @@ -45,6 +46,7 @@ const ( ProgUnwindDotnet10 = 0x9 ProgGoLabels = 0xa ProgUnwindBEAM = 0xb + ProgUnwindLuaJIT = 0xc ) const ( @@ -61,7 +63,7 @@ const ( const UnwindInfoMaxEntries = 0x4000 const ( - MetricIDBeginCumulative = 0x69 + MetricIDBeginCumulative = 0x6d ) const ( @@ -326,6 +328,11 @@ type V8ProcInfo struct { Codekind_baseline uint8 Pad_cgo_0 [2]byte } +type LuaJITProcInfo struct { + G2dispatch uint16 + Cur_L_offset uint16 + Cframe_size_jit uint16 +} const ( Sizeof_StackDelta = 0x4 @@ -397,6 +404,15 @@ const ( RubyFrameTypeGc = 0x4 ) +const ( + LJFFIFunc = 0xff1 + LJFileId = 0x2a + LJNormalFrame = 0x0 + LJGReport = 0xff2 + LJCframeSpaceX86 = 0x50 + LJCframeSpaceArm = 0xd0 +) + var MetricsTranslation = []metrics.MetricID{ 0x0: metrics.IDUnwindCallInterpreter, 0x1: metrics.IDUnwindErrZeroPC, @@ -486,11 +502,13 @@ var MetricsTranslation = []metrics.MetricID{ 0x5d: metrics.IDUnwindDotnetErrBadFP, 0x5e: metrics.IDUnwindDotnetErrCodeHeader, 0x5f: metrics.IDUnwindDotnetErrCodeTooLarge, - 0x62: metrics.IDUnwindRubyErrInvalidIseq, - 0x63: metrics.IDUnwindRubyErrReadMethodDef, - 0x64: metrics.IDUnwindRubyErrReadMethodType, - 0x65: metrics.IDUnwindRubyErrReadSvar, - 0x66: metrics.IDUnwindRubyErrReadRbasicFlags, - 0x67: metrics.IDUnwindRubyErrCmeMaxEp, - 0x68: metrics.IDUnwindErrBadDTVRead, + 0x66: metrics.IDUnwindRubyErrInvalidIseq, + 0x67: metrics.IDUnwindRubyErrReadMethodDef, + 0x68: metrics.IDUnwindRubyErrReadMethodType, + 0x69: metrics.IDUnwindRubyErrReadSvar, + 0x6a: metrics.IDUnwindRubyErrReadRbasicFlags, + 0x6b: metrics.IDUnwindRubyErrCmeMaxEp, + 0x6c: metrics.IDUnwindErrBadDTVRead, + 0x60: metrics.IDUnwindLuaJITAttempts, + 0x61: metrics.IDUnwindLuaJITErrNoProcInfo, } diff --git a/support/types_def.go b/support/types_def.go index 6c05aac10..a72cbaadd 100644 --- a/support/types_def.go +++ b/support/types_def.go @@ -13,6 +13,7 @@ import ( #include "./ebpf/types.h" #include "./ebpf/frametypes.h" #include "./ebpf/v8_tracer.h" +#include "./ebpf/luajit.h" */ import "C" @@ -28,6 +29,7 @@ const ( FrameMarkerPerl = C.FRAME_MARKER_PERL FrameMarkerV8 = C.FRAME_MARKER_V8 FrameMarkerDotnet = C.FRAME_MARKER_DOTNET + FrameMarkerLuaJIT = C.FRAME_MARKER_LUAJIT FrameMarkerBEAM = C.FRAME_MARKER_BEAM FrameMarkerGo = C.FRAME_MARKER_GO ) @@ -51,6 +53,7 @@ const ( ProgUnwindDotnet10 = C.PROG_UNWIND_DOTNET10 ProgGoLabels = C.PROG_GO_LABELS ProgUnwindBEAM = C.PROG_UNWIND_BEAM + ProgUnwindLuaJIT = C.PROG_UNWIND_LUAJIT ) const ( @@ -130,6 +133,7 @@ type PerlProcInfo C.PerlProcInfo type PyProcInfo C.PyProcInfo type RubyProcInfo C.RubyProcInfo type V8ProcInfo C.V8ProcInfo +type LuaJITProcInfo C.LuaJITProcInfo const ( Sizeof_StackDelta = C.sizeof_StackDelta @@ -207,6 +211,15 @@ const ( RubyFrameTypeGc = C.RUBY_FRAME_TYPE_GC ) +const ( + LJFFIFunc = C.LUAJIT_FFI_FUNC + LJFileId = C.LUAJIT_JIT_FILE_ID + LJNormalFrame = C.LUAJIT_NORMAL_FRAME + LJGReport = C.LUAJIT_G_REPORT + LJCframeSpaceX86 = C.LUAJIT_CFRAME_SPACE_X86_64 + LJCframeSpaceArm = C.LUAJIT_CFRAME_SPACE_AARCH64 +) + var MetricsTranslation = []metrics.MetricID{ C.metricID_UnwindCallInterpreter: metrics.IDUnwindCallInterpreter, C.metricID_UnwindErrZeroPC: metrics.IDUnwindErrZeroPC, @@ -303,4 +316,6 @@ var MetricsTranslation = []metrics.MetricID{ C.metricID_UnwindRubyErrReadRbasicFlags: metrics.IDUnwindRubyErrReadRbasicFlags, C.metricID_UnwindRubyErrCmeMaxEp: metrics.IDUnwindRubyErrCmeMaxEp, C.metricID_UnwindErrBadDTVRead: metrics.IDUnwindErrBadDTVRead, + C.metricID_UnwindLuaJITAttempts: metrics.IDUnwindLuaJITAttempts, + C.metricID_UnwindLuaJITErrNoProcInfo: metrics.IDUnwindLuaJITErrNoProcInfo, } diff --git a/testutils/helpers.go b/testutils/helpers.go new file mode 100644 index 000000000..a4331d34d --- /dev/null +++ b/testutils/helpers.go @@ -0,0 +1,162 @@ +package testutils // import "go.opentelemetry.io/ebpf-profiler/testutils" + +import ( + "bufio" + "context" + "errors" + "io" + "math" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/internal/log" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/reporter/samples" + "go.opentelemetry.io/ebpf-profiler/tracer" + tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" +) + +type MockIntervals struct{} + +func (f MockIntervals) MonitorInterval() time.Duration { return 1 * time.Second } +func (f MockIntervals) TracePollInterval() time.Duration { return 250 * time.Millisecond } +func (f MockIntervals) PIDCleanupInterval() time.Duration { return 1 * time.Second } +func (f MockIntervals) ExecutableUnloadDelay() time.Duration { return 1 * time.Second } + +type MockReporter struct{} + +func (f MockReporter) ExecutableKnown(_ libpf.FileID) bool { + return true +} + +type TraceEvent struct { + Trace *libpf.Trace + Meta *samples.TraceEventMeta +} + +type traceReporter struct { + traceEventChan chan<- TraceEvent +} + +func (tr *traceReporter) ReportTraceEvent(trace *libpf.Trace, meta *samples.TraceEventMeta) error { + tr.traceEventChan <- TraceEvent{ + Trace: trace, + Meta: meta, + } + return nil +} + +func StartTracer(ctx context.Context, t *testing.T, et tracertypes.IncludedTracers, + printBpfLogs bool) (<-chan TraceEvent, *tracer.Tracer) { + traceCh := make(chan TraceEvent) + tr := &traceReporter{ + traceEventChan: traceCh, + } + + trc, err := tracer.NewTracer(ctx, &tracer.Config{ + TraceReporter: tr, + Intervals: &MockIntervals{}, + IncludeTracers: et, + SamplesPerSecond: 20, + ProbabilisticInterval: 100, + ProbabilisticThreshold: 100, + OffCPUThreshold: uint32(math.MaxUint32 / 100), + VerboseMode: true, + }) + require.NoError(t, err) + + if printBpfLogs { + go readTracePipe(ctx) + } + + trc.StartPIDEventProcessor(ctx) + + err = trc.AttachTracer() + require.NoError(t, err) + log.Info("Attached tracer program") + + err = trc.EnableProfiling() + require.NoError(t, err) + log.Info("Enabled profiling") + + err = trc.AttachSchedMonitor() + require.NoError(t, err) + log.Info("Attached sched monitor") + + // Spawn monitors for the various result maps + ebpfTraceCh := make(chan *libpf.EbpfTrace) + + err = trc.StartMapMonitors(ctx, ebpfTraceCh) + require.NoError(t, err) + + go func() { + for { + select { + case trace := <-ebpfTraceCh: + if trace != nil { + trc.HandleTrace(trace) + } + case <-ctx.Done(): + return + } + } + }() + + return traceCh, trc +} + +func getTracePipe() (*os.File, error) { + for _, mnt := range []string{ + "/sys/kernel/debug/tracing", + "/sys/kernel/tracing", + "/tracing", + "/trace"} { + t, err := os.Open(mnt + "/trace_pipe") + if err == nil { + return t, nil + } + log.Errorf("Could not open trace_pipe at %s: %s", mnt, err) + } + return nil, os.ErrNotExist +} + +func readTracePipe(ctx context.Context) { + tp, err := getTracePipe() + if err != nil { + log.Warn("Could not open trace_pipe, check that debugfs is mounted") + return + } + + // When we're done kick ReadString out of blocked I/O. + go func() { + <-ctx.Done() + _ = tp.Close() + }() + + r := bufio.NewReader(tp) + for { + line, err := r.ReadString('\n') + if err != nil { + if errors.Is(err, io.EOF) { + continue + } + if errors.Is(err, os.ErrClosed) { + return + } + log.Error(err) + return + } + line = strings.TrimSpace(line) + if line != "" { + log.Infof("%s", line) + } + } +} + +func IsRoot() bool { + return os.Geteuid() == 0 +} diff --git a/tools/coredump/coredump.go b/tools/coredump/coredump.go index d653935da..3ce523fcb 100644 --- a/tools/coredump/coredump.go +++ b/tools/coredump/coredump.go @@ -177,7 +177,6 @@ func ExtractTraces(ctx context.Context, pr process.Process, debug bool, if err != nil { return nil, fmt.Errorf("failed to get Interpreter manager: %v", err) } - manager.SynchronizeProcess(pr) info := make([]ThreadInfo, 0, len(threadInfo)) diff --git a/tools/coredump/ebpfcode.h b/tools/coredump/ebpfcode.h index 7eb9cb82d..3292eb72b 100644 --- a/tools/coredump/ebpfcode.h +++ b/tools/coredump/ebpfcode.h @@ -36,6 +36,7 @@ void bpf_log(const char *fmt, ...) #include "../../support/ebpf/go_labels.ebpf.c" #include "../../support/ebpf/hotspot_tracer.ebpf.c" #include "../../support/ebpf/interpreter_dispatcher.ebpf.c" +#include "../../support/ebpf/luajit_tracer.ebpf.c" #include "../../support/ebpf/native_stack_trace.ebpf.c" #include "../../support/ebpf/perl_tracer.ebpf.c" #include "../../support/ebpf/php_tracer.ebpf.c" @@ -90,6 +91,7 @@ int bpf_tail_call(void *ctx, UNUSED void *map, int index) case PROG_UNWIND_DOTNET10: rc = unwind_dotnet10(ctx); break; case PROG_UNWIND_BEAM: rc = unwind_beam(ctx); break; case PROG_GO_LABELS: rc = go_labels(ctx); break; + case PROG_UNWIND_LUAJIT: rc = unwind_luajit(ctx); break; default: return -1; } __cgo_ctx->ret = rc; diff --git a/tools/coredump/ebpfhelpers.go b/tools/coredump/ebpfhelpers.go index 117fbef0f..b9a06ac91 100644 --- a/tools/coredump/ebpfhelpers.go +++ b/tools/coredump/ebpfhelpers.go @@ -110,7 +110,8 @@ func __bpf_map_lookup_elem(id C.u64, mapdef unsafe.Pointer, keyptr unsafe.Pointe case unsafe.Pointer(&C.dotnet_procs), unsafe.Pointer(&C.perl_procs), unsafe.Pointer(&C.php_procs), unsafe.Pointer(&C.py_procs), unsafe.Pointer(&C.hotspot_procs), unsafe.Pointer(&C.ruby_procs), - unsafe.Pointer(&C.v8_procs): + unsafe.Pointer(&C.v8_procs), + unsafe.Pointer(&C.luajit_procs): if innerMap, ok := ctx.maps[mapdef]; ok { if val, ok := innerMap[*(*C.u32)(keyptr)]; ok { return val diff --git a/tools/coredump/ebpfmaps.go b/tools/coredump/ebpfmaps.go index db9b83709..474e0292a 100644 --- a/tools/coredump/ebpfmaps.go +++ b/tools/coredump/ebpfmaps.go @@ -36,6 +36,10 @@ type ebpfMapsCoredump struct { var _ interpreter.EbpfHandler = &ebpfMapsCoredump{} +func (emc *ebpfMapsCoredump) CoredumpTest() bool { + return true +} + func (emc *ebpfMapsCoredump) RemoveReportedPID(libpf.PID) { } @@ -72,6 +76,8 @@ func (emc *ebpfMapsCoredump) UpdateProcData(t libpf.InterpreterType, pid libpf.P emc.ctx.addMap(unsafe.Pointer(&C.v8_procs), C.u32(pid), sliceBuffer(ptr, C.sizeof_V8ProcInfo)) case libpf.BEAM: emc.ctx.addMap(unsafe.Pointer(&C.beam_procs), C.u32(pid), sliceBuffer(ptr, C.sizeof_BEAMProcInfo)) + case libpf.LuaJIT: + emc.ctx.addMap(unsafe.Pointer(&C.luajit_procs), C.u32(pid), sliceBuffer(ptr, C.sizeof_LuaJITProcInfo)) } return nil } @@ -94,6 +100,8 @@ func (emc *ebpfMapsCoredump) DeleteProcData(t libpf.InterpreterType, pid libpf.P emc.ctx.delMap(unsafe.Pointer(&C.v8_procs), C.u32(pid)) case libpf.BEAM: emc.ctx.delMap(unsafe.Pointer(&C.beam_procs), C.u32(pid)) + case libpf.LuaJIT: + emc.ctx.delMap(unsafe.Pointer(&C.luajit_procs), C.u32(pid)) } return nil } diff --git a/tools/dis2go.py b/tools/dis2go.py new file mode 100755 index 000000000..4a4982342 --- /dev/null +++ b/tools/dis2go.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import re +import sys + +def transform_disassembly(lines): + """ + Transform llvm-objdump disassembly to a Go byte array literal. + Written by Claude. + + This function: + 1. Removes offset prefixes (like "15e5b:") + 2. Adds "0x" prefix to each byte and comma separators + 3. Comments out the disassembly parts with "//" + + Args: + lines: A list of input lines to transform + + Returns: + A list of transformed lines + """ + result = [] + + for line in lines: + # Skip empty lines + if not line.strip(): + continue + + # Extract the parts using regex + # Match the address prefix, then bytes, then the assembly instruction + match = re.match(r'^\s*[0-9a-f]+:\s+([0-9a-f\s]+)(.+)$', line) + if match: + # Get the machine code bytes + bytes_str = match.group(1).strip() + # Get the assembly instruction + asm_instr = match.group(2).strip() + + # Split the bytes and add 0x prefix and commas + bytes_list = [f"0x{b}" for b in bytes_str.split()] + formatted_bytes = ", ".join(bytes_list) + "," + + # Pad with spaces to align comments + pad_length = max(40 - len(formatted_bytes), 1) + padding = " " * pad_length + + # Create the Go byte array line with commented assembly + result.append(f"{formatted_bytes}{padding}// {asm_instr}") + else: + # If the line doesn't match our expected format, keep it as a comment + result.append(f"// {line.strip()}") + + return result + +def main(): + # Read from stdin + lines = sys.stdin.readlines() + + # Transform the lines + transformed_lines = transform_disassembly(lines) + + # Write to stdout + sys.stdout.write("\n".join(transformed_lines)) + sys.stdout.write("\n") # Add a final newline + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/errors-codegen/errors.json b/tools/errors-codegen/errors.json index e9850c747..e79663552 100644 --- a/tools/errors-codegen/errors.json +++ b/tools/errors-codegen/errors.json @@ -364,5 +364,30 @@ "id": 7006, "name": "beam_range_search_exhausted", "description": "BEAM: Ran out of iterations searching for the current code header" + }, + { + "id": 7007, + "name": "luajit_no_proc_info", + "description": "LuaJIT: No entry for this process exists in the LuaJIT process info array" + }, + { + "id": 7008, + "name": "luajit_read_lua_context", + "description": "LuaJIT: Unable to read the Lua context" + }, + { + "id": 7009, + "name": "luajit_frame_read", + "description": "LuaJIT: Unable to read the Lua frame" + }, + { + "id": 7010, + "name": "luajit_l_mismatch", + "description": "LuaJIT: context pointer validity check failed" + }, + { + "id": 7011, + "name": "luajit_invalid_pc", + "description": "LuaJIT: PC exceeds 24 bits" } ] diff --git a/tracer/tracer.go b/tracer/tracer.go index 3a9f09258..45de0ab14 100644 --- a/tracer/tracer.go +++ b/tracer/tracer.go @@ -444,6 +444,11 @@ func initializeMapsAndPrograms(kmod *kallsyms.Module, cfg *Config) ( name: "unwind_beam", enable: cfg.IncludeTracers.Has(types.BEAMTracer), }, + { + progID: uint32(support.ProgUnwindLuaJIT), + name: "unwind_luajit", + enable: cfg.IncludeTracers.Has(types.LuaJITTracer), + }, } if err = loadPerfUnwinders(coll, ebpfProgs, ebpfMaps["perf_progs"], tailCallProgs, diff --git a/tracer/types/parse.go b/tracer/types/parse.go index 78621968a..d06cf7bd1 100644 --- a/tracer/types/parse.go +++ b/tracer/types/parse.go @@ -22,6 +22,7 @@ const ( RubyTracer V8Tracer DotnetTracer + LuaJITTracer GoTracer Labels BEAMTracer @@ -38,6 +39,7 @@ var tracerTypeToName = map[tracerType]string{ RubyTracer: "ruby", V8Tracer: "v8", DotnetTracer: "dotnet", + LuaJITTracer: "luajit", GoTracer: "go", Labels: "labels", BEAMTracer: "beam", diff --git a/x86helpers/x86_helpers.go b/x86helpers/x86_helpers.go new file mode 100644 index 000000000..a322d9566 --- /dev/null +++ b/x86helpers/x86_helpers.go @@ -0,0 +1,33 @@ +// Copyright 2024 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +// This package contains a series of helper functions that are useful for x86 disassembly. +package x86helpers // import "go.opentelemetry.io/ebpf-profiler/x86helpers" + +import "slices" + +var endbr64 = [4]byte{0xf3, 0x0f, 0x1e, 0xfa} + +// On some binaries the function starts like this: +// +// 0x0000000000012860 <+0>: f3 0f 1e fa endbr64 +// 0x0000000000012864 <+4>: 41 55 push %r13 +// +// This is some kind of stack smashing indirect jump protection, treat it as a nop, +// x86asm doesn't know how to handle it. +// +//nolint:gocritic +func SkipEndBranch(b []byte) ([]byte, int64) { + if slices.Equal(b[0:4], endbr64[:]) { + return b[4:], 4 + } + return b, 0 +}