diff --git a/Makefile b/Makefile index f4d7cb565c..baa19cc76f 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,6 @@ VMLINUX := vmlinux.h BPF_ROOT := bpf BPF_SRC := $(BPF_ROOT)/unwinders/native.bpf.c OUT_BPF_DIR := pkg/profiler/cpu/bpf/programs/objects/$(ARCH) -# TODO(kakkoyun): DRY. OUT_BPF := $(OUT_BPF_DIR)/native.bpf.o OUT_RBPERF := $(OUT_BPF_DIR)/rbperf.bpf.o OUT_PYPERF := $(OUT_BPF_DIR)/pyperf.bpf.o @@ -163,7 +162,6 @@ ifndef DOCKER $(OUT_BPF): $(BPF_SRC) libbpf | $(OUT_DIR) mkdir -p $(OUT_BPF_DIR) $(OUT_BPF_CONTAINED_DIR) $(MAKE) -C bpf build - # TODO(kakkoyun): DRY. cp bpf/out/$(ARCH)/native.bpf.o $(OUT_BPF) cp bpf/out/$(ARCH)/rbperf.bpf.o $(OUT_RBPERF) cp bpf/out/$(ARCH)/pyperf.bpf.o $(OUT_PYPERF) diff --git a/bpf/Makefile b/bpf/Makefile index f3059d2314..0aae8e3e5d 100644 --- a/bpf/Makefile +++ b/bpf/Makefile @@ -14,7 +14,6 @@ OUT_DIR ?= ../dist OUT_BPF_BASE_DIR := out OUT_BPF_DIR := $(OUT_BPF_BASE_DIR)/$(ARCH) OUT_BPF := $(OUT_BPF_DIR)/native.bpf.o -# TODO(kakkoyun): DRY. OUT_RBPERF := $(OUT_BPF_DIR)/rbperf.bpf.o OUT_PYPERF := $(OUT_BPF_DIR)/pyperf.bpf.o OUT_PID_NAMESPACE_DETECTOR := $(OUT_BPF_DIR)/pid_namespace.bpf.o @@ -24,7 +23,6 @@ BPF_BUNDLE := $(OUT_DIR)/parca-agent.bpf.tar.gz LIBBPF_HEADERS := $(OUT_DIR)/libbpf/$(ARCH)/usr/include VMLINUX_INCLUDE_PATH := $(SHORT_ARCH) -# TODO(kakkoyun): DRY. BPF_SRC := unwinders/native.bpf.c RBPERF_SRC := unwinders/rbperf.bpf.c PYPERF_SRC := unwinders/pyperf.bpf.c diff --git a/bpf/unwinders/pyperf.bpf.c b/bpf/unwinders/pyperf.bpf.c index c6e200f91b..aca217450c 100644 --- a/bpf/unwinders/pyperf.bpf.c +++ b/bpf/unwinders/pyperf.bpf.c @@ -48,6 +48,20 @@ struct { __type(value, PythonVersionOffsets); } version_specific_offsets SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 12); // arbitrary + __type(key, u32); + __type(value, LibcOffsets); +} musl_offsets SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 12); // arbitrary + __type(key, u32); + __type(value, LibcOffsets); +} glibc_offsets SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); @@ -80,7 +94,64 @@ struct { } \ }) -static __always_inline long unsigned int read_tls_base(struct task_struct *task) { +// tls_read reads from the TLS associated with the provided key depending on the libc implementation. +static inline __attribute__((__always_inline__)) int tls_read(void *tls_base, InterpreterInfo *interpreter_info, void **out) { + LibcOffsets *libc_offsets; + void *tls_addr = NULL; + int key = interpreter_info->tls_key; + switch (interpreter_info->libc_implementation) { + case LIBC_IMPLEMENTATION_GLIBC: + // Read the offset from the corresponding map. + libc_offsets = bpf_map_lookup_elem(&glibc_offsets, &interpreter_info->libc_offset_index); + if (libc_offsets == NULL) { + LOG("[error] libc_offsets for glibc is NULL"); + return -1; + } +#if __TARGET_ARCH_x86 + + tls_addr = tls_base + libc_offsets->pthread_block + (key * libc_offsets->pthread_key_data_size) + libc_offsets->pthread_key_data; +#elif __TARGET_ARCH_arm64 + tls_addr = + tls_base - libc_offsets->pthread_size + libc_offsets->pthread_block + (key * libc_offsets->pthread_key_data_size) + libc_offsets->pthread_key_data; +#else +#error "Unsupported platform" +#endif + break; + case LIBC_IMPLEMENTATION_MUSL: + // Read the offset from the corresponding map. + libc_offsets = bpf_map_lookup_elem(&musl_offsets, &interpreter_info->libc_offset_index); + if (libc_offsets == NULL) { + LOG("[error] libc_offsets for musl is NULL"); + return -1; + } +#if __TARGET_ARCH_x86 + if (bpf_probe_read_user(&tls_addr, sizeof(tls_addr), tls_base + libc_offsets->pthread_block)) { + return -1; + } + tls_addr = tls_addr + key * libc_offsets->pthread_key_data_size; +#elif __TARGET_ARCH_arm64 + if (bpf_probe_read_user(&tls_addr, sizeof(tls_addr), tls_base - libc_offsets->pthread_size + libc_offsets->pthread_block)) { + return -1; + } + tls_addr = key * libc_offsets->pthread_key_data_size; +#else +#error "Unsupported platform" +#endif + break; + default: + LOG("[error] unknown libc_implementation %d", interpreter_info->libc_implementation); + return -1; + } + + LOG("tls_read key %d from address 0x%lx", key, (unsigned long)tls_addr); + if (bpf_probe_read(out, sizeof(*out), tls_addr)) { + LOG("failed to read 0x%lx from TLS", (unsigned long)tls_addr); + return -1; + } + return 0; +} + +static inline __attribute__((__always_inline__)) long unsigned int read_tls_base(struct task_struct *task) { long unsigned int tls_base; // This changes depending on arch and kernel version. // task->thread.fs, task->thread.uw.tp_value, etc. @@ -108,7 +179,6 @@ int unwind_python_stack(struct bpf_perf_event_data *ctx) { return 1; } - // TODO(kakkoyun) : DRY. u64 pid_tgid = bpf_get_current_pid_tgid(); pid_t pid = pid_tgid >> 32; pid_t tid = pid_tgid; @@ -122,11 +192,6 @@ int unwind_python_stack(struct bpf_perf_event_data *ctx) { return 0; } - if (interpreter_info->thread_state_addr == 0) { - LOG("[error] interpreter_info.thread_state_addr was NULL"); - return 0; - } - LOG("[start]"); LOG("[event] pid=%d tid=%d", pid, tid); @@ -157,50 +222,68 @@ int unwind_python_stack(struct bpf_perf_event_data *ctx) { // Fetch thread state. - // GDB: ((PyThreadState *)_PyRuntime.gilstate.tstate_current) - LOG("interpreter_info->thread_state_addr 0x%llx", interpreter_info->thread_state_addr); - int err = bpf_probe_read_user(&state->thread_state, sizeof(state->thread_state), (void *)(long)interpreter_info->thread_state_addr); - if (err != 0) { - LOG("[error] failed to read interpreter_info->thread_state_addr with %d", err); - goto submit_without_unwinding; - } - if (state->thread_state == 0) { - LOG("[error] thread_state was NULL"); - goto submit_without_unwinding; + if (interpreter_info->thread_state_addr != 0) { + LOG("interpreter_info->thread_state_addr 0x%llx", interpreter_info->thread_state_addr); + int err = bpf_probe_read_user(&state->thread_state, sizeof(state->thread_state), (void *)(long)interpreter_info->thread_state_addr); + if (err != 0) { + LOG("[error] failed to read interpreter_info->thread_state_addr with %d", err); + goto submit_without_unwinding; + } + LOG("thread_state 0x%llx", state->thread_state); } - LOG("thread_state 0x%llx", state->thread_state); - struct task_struct *task = (struct task_struct *)bpf_get_current_task(); - long unsigned int tls_base = read_tls_base(task); - LOG("tls_base 0x%llx", (void *)tls_base); + if (interpreter_info->use_tls) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + long unsigned int tls_base = read_tls_base(task); + LOG("tls_base 0x%llx", (void *)tls_base); + + // TODO(kakkoyun): Read TLS key in here instead of user-space. + // int key; + // if (bpf_probe_read(&key, sizeof(key), interpreter_info->tls_key_addr)) { + // LOG("[error] failed to read TLS key from 0x%lx", (unsigned long)interpreter_info->tls_key_addr); + // goto submit_without_unwinding; + // } + if (tls_read((void *)tls_base, interpreter_info, &state->thread_state)) { + LOG("[error] failed to read thread state from TLS 0x%lx", (unsigned long)interpreter_info->tls_key); + goto submit_without_unwinding; + } + + if (state->thread_state == 0) { + LOG("[error] thread_state was NULL"); + goto submit_without_unwinding; + } + LOG("thread_state 0x%llx", state->thread_state); + } GET_OFFSETS(); // Fetch the thread id. + LOG("offsets->py_thread_state.thread_id %d", offsets->py_thread_state.thread_id); pthread_t pthread_id; - bpf_probe_read_user(&pthread_id, sizeof(pthread_id), state->thread_state + offsets->py_thread_state.thread_id); + if (bpf_probe_read_user(&pthread_id, sizeof(pthread_id), state->thread_state + offsets->py_thread_state.thread_id)) { + LOG("[error] failed to read thread_state->thread_id"); + goto submit_without_unwinding; + } + LOG("pthread_id %lu", pthread_id); - // 0x10 = offsetof(tcbhead_t, self) for glibc x86. - // pthread_t current_pthread_id; - // bpf_probe_read_user(¤t_pthread_id, sizeof(current_pthread_id), (void *)(tls_base + 0x10)); - // LOG("current_pthread_id %lu", current_pthread_id); - // if (pthread_id != current_pthread_id) { - // LOG("[error] pthread_id %lu != current_pthread_id %lu", pthread_id, current_pthread_id); - // goto submit_without_unwinding; - // } - // state->current_pthread = current_pthread_id; state->current_pthread = pthread_id; // Get pointer to top frame from PyThreadState. if (offsets->py_thread_state.frame > -1) { LOG("offsets->py_thread_state.frame %d", offsets->py_thread_state.frame); - bpf_probe_read_user(&state->frame_ptr, sizeof(void *), state->thread_state + offsets->py_thread_state.frame); + if (bpf_probe_read_user(&state->frame_ptr, sizeof(void *), state->thread_state + offsets->py_thread_state.frame)) { + LOG("[error] failed to read thread_state->frame"); + goto submit_without_unwinding; + } } else { LOG("offsets->py_thread_state.cframe %d", offsets->py_thread_state.cframe); void *cframe; - bpf_probe_read_user(&cframe, sizeof(cframe), (void *)(state->thread_state + offsets->py_thread_state.cframe)); + if (bpf_probe_read_user(&cframe, sizeof(cframe), (void *)(state->thread_state + offsets->py_thread_state.cframe))) { + LOG("[error] failed to read thread_state->cframe"); + goto submit_without_unwinding; + } if (cframe == 0) { LOG("[error] cframe was NULL"); goto submit_without_unwinding; diff --git a/bpf/unwinders/pyperf.h b/bpf/unwinders/pyperf.h index cd9156fe3e..aa9b7cb653 100644 --- a/bpf/unwinders/pyperf.h +++ b/bpf/unwinders/pyperf.h @@ -12,11 +12,22 @@ #define PYPERF_STACK_WALKING_PROGRAM_IDX 0 +enum libc_implementation { + LIBC_IMPLEMENTATION_GLIBC = 0, + LIBC_IMPLEMENTATION_MUSL = 1, +}; + typedef struct { // u64 start_time; // u64 interpreter_addr; u64 thread_state_addr; + u64 tls_key; u32 py_version_offset_index; + u32 libc_offset_index; + enum libc_implementation libc_implementation; + + _Bool use_tls; + // TODO(kakkoyun): bool use_runtime_debug_offsets; } InterpreterInfo; enum python_stack_status { @@ -127,3 +138,10 @@ typedef struct { PyTupleObject py_tuple_object; PyTypeObject py_type_object; } PythonVersionOffsets; + +typedef struct { + s64 pthread_size; + s64 pthread_block; + s64 pthread_key_data; + s64 pthread_key_data_size; +} LibcOffsets; diff --git a/go.mod b/go.mod index 3f55296746..68800129b2 100644 --- a/go.mod +++ b/go.mod @@ -24,12 +24,12 @@ require ( github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.1 - github.com/klauspost/compress v1.17.6 + github.com/klauspost/compress v1.17.7 github.com/minio/highwayhash v1.0.2 github.com/oklog/run v1.1.0 github.com/opencontainers/runtime-spec v1.2.0 github.com/parca-dev/parca v0.20.0 - github.com/parca-dev/runtime-data v0.0.0-20240221144835-838a856ad496 + github.com/parca-dev/runtime-data v0.0.0-20240225202746-241008d5f5c3 github.com/planetscale/vtprotobuf v0.6.0 github.com/prometheus/client_golang v1.18.0 github.com/prometheus/common v0.47.0 @@ -201,11 +201,11 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/metric v1.23.1 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.17.0 // indirect diff --git a/go.sum b/go.sum index 5b53229ee3..b3ee594964 100644 --- a/go.sum +++ b/go.sum @@ -386,8 +386,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -495,8 +495,8 @@ github.com/oracle/oci-go-sdk/v65 v65.53.0 h1:/h+rzaRw7W1eSTeDLhSMTRnyXg1oj5NTPeB github.com/oracle/oci-go-sdk/v65 v65.53.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/parca-dev/parca v0.20.0 h1:G/TdZLbZEArZzd86rI2RqMpUtz0verlvO3+NDP/d0m0= github.com/parca-dev/parca v0.20.0/go.mod h1:dKRKjo1MK83iEUygyINUYTAriHICGYejD4EWm5MGT+0= -github.com/parca-dev/runtime-data v0.0.0-20240221144835-838a856ad496 h1:sTUSBmbMriumcql8S8UwHtfpzYO2GfaivNEs9eY0Ty4= -github.com/parca-dev/runtime-data v0.0.0-20240221144835-838a856ad496/go.mod h1:NRpa9nozMNUeyw6p6KeuSx+leWBMoJJE7+mN7XUNdpA= +github.com/parca-dev/runtime-data v0.0.0-20240225202746-241008d5f5c3 h1:hlxBPG8wcI0fd2RLJHKcPIFO36k+9mEMpYb3e8NFbSg= +github.com/parca-dev/runtime-data v0.0.0-20240225202746-241008d5f5c3/go.mod h1:zSTEFju13hAb35sGVtDwWXXHhRPoVQjDnFEpltj6du4= github.com/parquet-go/parquet-go v0.19.0 h1:xtHOBIE0/8CRhmf06V1GJ7q3qARY2/kXiSweFlscwUQ= github.com/parquet-go/parquet-go v0.19.0/go.mod h1:6pu/Ca02WRyWyF6jbY1KceESGBZMsRMSijjLbajXaG8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -683,8 +683,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -729,8 +729,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -785,8 +785,8 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/pkg/profiler/cpu/bpf/maps/maps.go b/pkg/profiler/cpu/bpf/maps/maps.go index f80cea4ec0..2927a81512 100644 --- a/pkg/profiler/cpu/bpf/maps/maps.go +++ b/pkg/profiler/cpu/bpf/maps/maps.go @@ -12,6 +12,7 @@ // limitations under the License. // +//nolint:dupl package bpfmaps import "C" @@ -24,6 +25,7 @@ import ( "fmt" "os" "path" + goruntime "runtime" "strconv" "strings" "sync" @@ -38,6 +40,9 @@ import ( "github.com/prometheus/procfs" "golang.org/x/exp/constraints" + "github.com/parca-dev/runtime-data/pkg/libc" + "github.com/parca-dev/runtime-data/pkg/libc/glibc" + "github.com/parca-dev/runtime-data/pkg/libc/musl" "github.com/parca-dev/runtime-data/pkg/python" "github.com/parca-dev/runtime-data/pkg/ruby" @@ -52,6 +57,7 @@ import ( "github.com/parca-dev/parca-agent/pkg/profiler/pyperf" "github.com/parca-dev/parca-agent/pkg/profiler/rbperf" "github.com/parca-dev/parca-agent/pkg/runtime" + runtimelibc "github.com/parca-dev/parca-agent/pkg/runtime/libc" "github.com/parca-dev/parca-agent/pkg/stack/unwind" ) @@ -72,6 +78,8 @@ const ( // pyperf maps. PythonPIDToInterpreterInfoMapName = "pid_to_interpreter_info" PythonVersionSpecificOffsetMapName = "version_specific_offsets" + PythonGlibcOffsetsMapName = "glibc_offsets" + PythonMuslOffsetsMapName = "musl_offsets" UnwindInfoChunksMapName = "unwind_info_chunks" UnwindTablesMapName = "unwind_tables" @@ -260,18 +268,17 @@ type stackTraceWithLength struct { func New( logger log.Logger, - byteOrder binary.ByteOrder, - arch elf.Machine, - modules map[ProfilerModuleType]*libbpf.Module, metrics *Metrics, + modules map[ProfilerModuleType]*libbpf.Module, + ofp *objectfile.Pool, processCache *ProcessCache, syncedInterpreters *cache.Cache[int, runtime.Interpreter], - ofp *objectfile.Pool, ) (*Maps, error) { if modules[NativeModule] == nil { return nil, errors.New("nil nativeModule") } + arch := getArch() var compactUnwindRowSizeBytes int switch arch { case elf.EM_AARCH64: @@ -291,7 +298,7 @@ func New( nativeModule: modules[NativeModule], rbperfModule: modules[RbperfModule], pyperfModule: modules[PyperfModule], - byteOrder: byteOrder, + byteOrder: binary.LittleEndian, processCache: processCache, mappingInfoMemory: mappingInfoMemory, compactUnwindRowSizeBytes: compactUnwindRowSizeBytes, @@ -519,7 +526,7 @@ func (m *Maps) setPyperfIntepreterInfo(pid int, interpInfo pyperf.InterpreterInf return nil } -func (m *Maps) setPyperfVersionOffsets(versionOffsets map[python.Key]*python.Layout) error { +func (m *Maps) setPyperfOffsets(versionOffsets map[python.Key]*python.Layout) error { if m.pyperfModule == nil { return nil } @@ -551,6 +558,85 @@ func (m *Maps) setPyperfVersionOffsets(versionOffsets map[python.Key]*python.Lay return nil } +func (m *Maps) setLibcOffsets() error { + if m.pyperfModule == nil { + return nil + } + + glibcOffsets, err := glibc.GetLayouts() + if err != nil { + return fmt.Errorf("get glibc version offsets: %w", err) + } + + if len(glibcOffsets) == 0 { + return errors.New("no glibc offsets provided") + } + + var errs error + errs = errors.Join(errs, m.setGlibcOffsets(glibcOffsets)) + + muslOffsets, err := musl.GetLayouts() + if err != nil { + return fmt.Errorf("get musl version offsets: %w", err) + } + + if len(muslOffsets) == 0 { + return errors.New("no musl offsets provided") + } + + return errors.Join(errs, m.setMuslOffsets(muslOffsets)) +} + +func (m *Maps) setGlibcOffsets(offsets map[glibc.Key]*libc.Layout) error { + glibcOffsetMap, err := m.pyperfModule.GetMap(PythonGlibcOffsetsMapName) + if err != nil { + return fmt.Errorf("get map version_specific_offsets: %w", err) + } + + buf := new(bytes.Buffer) + for k, v := range offsets { + buf.Grow(int(unsafe.Sizeof(v))) + err = binary.Write(buf, binary.LittleEndian, v) + if err != nil { + level.Debug(m.logger).Log("msg", "write glibcOffsets to buffer", "err", err) + continue + } + key := uint32(k.Index) + err = glibcOffsetMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&buf.Bytes()[0])) + if err != nil { + level.Debug(m.logger).Log("msg", "update map glibc_offsets", "err", err) + continue + } + buf.Reset() + } + return nil +} + +func (m *Maps) setMuslOffsets(offsets map[musl.Key]*libc.Layout) error { + muslOffsetMap, err := m.pyperfModule.GetMap(PythonMuslOffsetsMapName) + if err != nil { + return fmt.Errorf("get map version_specific_offsets: %w", err) + } + + buf := new(bytes.Buffer) + for k, v := range offsets { + buf.Grow(int(unsafe.Sizeof(v))) + err = binary.Write(buf, binary.LittleEndian, v) + if err != nil { + level.Debug(m.logger).Log("msg", "write muslOffsets to buffer", "err", err) + continue + } + key := uint32(k.Index) + err = muslOffsetMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&buf.Bytes()[0])) + if err != nil { + level.Debug(m.logger).Log("msg", "update map musl_offsets", "err", err) + continue + } + buf.Reset() + } + return nil +} + func (m *Maps) SetInterpreterData() error { if m.pyperfModule == nil && m.rbperfModule == nil { return nil @@ -586,10 +672,15 @@ func (m *Maps) SetInterpreterData() error { return fmt.Errorf("get python version offsets: %w", err) } - err = m.setPyperfVersionOffsets(layouts) + err = m.setPyperfOffsets(layouts) if err != nil { return fmt.Errorf("set python version offsets: %w", err) } + + err = m.setLibcOffsets() + if err != nil { + return fmt.Errorf("set libc version offsets: %w", err) + } } return nil @@ -820,11 +911,21 @@ func (m *Maps) AddInterpreter(pid int, interpreter runtime.Interpreter) error { return nil } - i, err := m.indexForInterpreter(interpreter) + version, err := semver.NewVersion(interpreter.Version) + if err != nil { + return fmt.Errorf("parse version: %w", err) + } + + offsetIdx, err := m.indexForInterpreter(interpreter) if err != nil { return fmt.Errorf("index for interpreter version: %w", err) } + libcIdx, err := m.indexForLibc(interpreter) + if err != nil { + return fmt.Errorf("index for libc version: %w", err) + } + switch interpreter.Type { case runtime.InterpreterRuby: pats := strings.Split(interpreter.Version, ".") @@ -845,20 +946,28 @@ func (m *Maps) AddInterpreter(pid int, interpreter runtime.Interpreter) error { procData := rbperf.ProcessData{ RbFrameAddr: interpreter.MainThreadAddress, StartTime: 0, // Unused as of now. - RbVersion: i, + RbVersion: offsetIdx, AccountForVariableWidth: accountForVariableWidth, } - level.Debug(m.logger).Log("msg", "Ruby Version Offset", "pid", pid, "version_offset_index", i) + level.Debug(m.logger).Log("msg", "Ruby Version Offset", "pid", pid, "version_offset_index", offsetIdx) if err := m.setRbperfProcessData(pid, procData); err != nil { return err } m.syncedInterpreters.Add(pid, interpreter) case runtime.InterpreterPython: + var libcImplementation int32 + if interpreter.LibcInfo != nil { + libcImplementation = int32(interpreter.LibcInfo.Implementation) + } interpreterInfo := pyperf.InterpreterInfo{ ThreadStateAddr: interpreter.MainThreadAddress, - PyVersionOffsetIndex: i, + TLSKey: interpreter.TLSKey, + PyVersionOffsetIndex: offsetIdx, + LibcOffsetIndex: libcIdx, + LibcImplementation: libcImplementation, + UseTLS: mustNewConstraint(">= 3.12.0-0").Check(version), } - level.Debug(m.logger).Log("msg", "Python Version Offset", "pid", pid, "version_offset_index", i) + level.Debug(m.logger).Log("msg", "Python Version Offset", "pid", pid, "version_offset_index", offsetIdx) if err := m.setPyperfIntepreterInfo(pid, interpreterInfo); err != nil { return err } @@ -892,6 +1001,27 @@ func (m *Maps) indexForInterpreter(interpreter runtime.Interpreter) (uint32, err } } +func (m *Maps) indexForLibc(interpreter runtime.Interpreter) (uint32, error) { + if interpreter.LibcInfo == nil { + return 0, nil + } + switch interpreter.LibcInfo.Implementation { + case runtimelibc.LibcGlibc: + k, _, err := glibc.GetLayout(interpreter.LibcInfo.Version) + if err != nil { + return 0, fmt.Errorf("failed to get glibc layout %s: %w", interpreter.LibcInfo.Version, err) + } + return uint32(k.Index), nil + case runtimelibc.LibcMusl: + k, _, err := musl.GetLayout(interpreter.LibcInfo.Version) + if err != nil { + return 0, fmt.Errorf("failed to get musl layout %s: %w", interpreter.LibcInfo.Version, err) + } + return uint32(k.Index), nil + } + return 0, fmt.Errorf("invalid libc name: %d", interpreter.LibcInfo.Implementation) +} + func (m *Maps) SetDebugPIDs(pids []int) error { // Clean up old debug pids. it := m.debugPIDs.Iterator() @@ -1708,10 +1838,12 @@ func (m *Maps) setUnwindTableForMapping(buf *profiler.EfficientBuffer, pid int, } executableID := m.executableID - if err := m.unwindShards.Update( - unsafe.Pointer(&executableID), - unsafe.Pointer(&unwindShardsValBuf.Bytes()[0])); err != nil { - return fmt.Errorf("failed to update unwind shard: %w", err) + if b := unwindShardsValBuf.Bytes(); len(b) > 0 { + if err := m.unwindShards.Update( + unsafe.Pointer(&executableID), + unsafe.Pointer(&b[0])); err != nil { + return fmt.Errorf("failed to update unwind shard: %w", err) + } } m.executableID++ @@ -1720,3 +1852,22 @@ func (m *Maps) setUnwindTableForMapping(buf *profiler.EfficientBuffer, pid int, return nil } + +func getArch() elf.Machine { + switch goruntime.GOARCH { + case "arm64": + return elf.EM_AARCH64 + case "amd64": + return elf.EM_X86_64 + default: + return elf.EM_NONE + } +} + +func mustNewConstraint(v string) *semver.Constraints { + c, err := semver.NewConstraint(v) + if err != nil { + panic(err) + } + return c +} diff --git a/pkg/profiler/cpu/cpu.go b/pkg/profiler/cpu/cpu.go index 2b68b1c938..1a34163292 100644 --- a/pkg/profiler/cpu/cpu.go +++ b/pkg/profiler/cpu/cpu.go @@ -25,7 +25,6 @@ import ( "fmt" "os" "regexp" - goruntime "runtime" "strings" "sync" "syscall" @@ -277,17 +276,14 @@ func loadBPFModules(logger log.Logger, reg prometheus.Registerer, memlockRlimit bpfmaps.PyperfModule: pyperf, } - arch := getArch() // Maps must be initialized before loading the BPF code. bpfMaps, err := bpfmaps.New( logger, - binary.LittleEndian, - arch, - modules, bpfmapMetrics, + modules, + ofp, bpfmapsProcessCache, syncedIntepreters, - ofp, ) if err != nil { return nil, nil, fmt.Errorf("failed to initialize eBPF maps: %w", err) @@ -632,7 +628,6 @@ func processEventBatcher(ctx context.Context, eventsChannel <-chan int, duration } } -// TODO(kakkoyun): Combine with process information discovery. func (p *CPU) watchProcesses(ctx context.Context, pfs procfs.FS, matchers []*regexp.Regexp) { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() @@ -1216,17 +1211,6 @@ func preprocessRawData(rawData map[profileKey]map[bpfprograms.CombinedStack]uint return res } -func getArch() elf.Machine { - switch goruntime.GOARCH { - case "arm64": - return elf.EM_AARCH64 - case "amd64": - return elf.EM_X86_64 - default: - return elf.EM_NONE - } -} - type errorTracker struct { logger log.Logger errorEncounters prometheus.Counter diff --git a/pkg/profiler/pyperf/headers.go b/pkg/profiler/pyperf/headers.go index 74146ad8c7..fe43ce41e1 100644 --- a/pkg/profiler/pyperf/headers.go +++ b/pkg/profiler/pyperf/headers.go @@ -18,5 +18,9 @@ type InterpreterInfo struct { // u64 start_time; // InterpreterAddr uint64 ThreadStateAddr uint64 + TLSKey uint64 PyVersionOffsetIndex uint32 + LibcOffsetIndex uint32 + LibcImplementation int32 + UseTLS bool } diff --git a/pkg/profiler/rbperf/headers.go b/pkg/profiler/rbperf/headers.go index 3c4c0de6d4..32891b11b7 100644 --- a/pkg/profiler/rbperf/headers.go +++ b/pkg/profiler/rbperf/headers.go @@ -31,20 +31,3 @@ type ProcessData struct { RbVersion u32 AccountForVariableWidth bool } - -type RubyVersionOffsets struct { - MajorVersion int32 - MinorVersion int32 - PatchVersion int32 - VMOffset int32 - VMSizeOffset int32 - ControlFrameSizeof int32 - CfpOffset int32 - LabelOffset int32 - PathFlavour int32 - LineInfoSizeOffset int32 - LineInfoTableOffset int32 - LinenoOffset int32 - MainThreadOffset int32 - EcOffset int32 -} diff --git a/pkg/runtime/libc/libc.go b/pkg/runtime/libc/libc.go new file mode 100644 index 0000000000..0b0d6b166b --- /dev/null +++ b/pkg/runtime/libc/libc.go @@ -0,0 +1,210 @@ +// Copyright 2022-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 libc + +import ( + "errors" + "fmt" + "io" + "os" + "path" + "regexp" + "strconv" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/prometheus/procfs" + "github.com/xyproto/ainur" +) + +type LibcImplementation int32 + +const ( + LibcGlibc LibcImplementation = iota + LibcMusl +) + +type LibcInfo struct { + Implementation LibcImplementation + Version *semver.Version +} + +func NewLibcInfo(proc procfs.Proc) (*LibcInfo, error) { + maps, err := proc.ProcMaps() + if err != nil { + return nil, fmt.Errorf("error reading process maps: %w", err) + } + var ( + imp LibcImplementation + libcPath string + found bool + ) + for _, m := range maps { + if pathname := m.Pathname; pathname != "" { + if isGlibc(pathname) { + imp = LibcGlibc + libcPath = pathname + found = true + break + } + if isMusl(pathname) { + imp = LibcMusl + libcPath = pathname + found = true + break + } + } + } + if !found { + return nil, errors.New("no libc implementation found") + } + + f, err := os.Open(absolutePath(proc, libcPath)) + if err != nil { + return nil, fmt.Errorf("open libc file: %w", err) + } + defer f.Close() + + // It is easier to get the version of the libc implementation by running the libc itself, + // rather than scanning the file and matching the version string. + var version *semver.Version + switch imp { + case LibcGlibc: + version, err = glibcVersion(f) + if err != nil { + return nil, fmt.Errorf("glibc version: %w", err) + } + case LibcMusl: + version, err = muslVersion(f) + if err != nil { + return nil, fmt.Errorf("musl version: %w", err) + } + } + + return &LibcInfo{ + Implementation: imp, + Version: version, + }, nil +} + +// ❯ docker run -it --rm ubuntu sh -c 'ldd /usr/bin/ls' +// +// linux-vdso.so.1 (0x00007ffeb337b000) +// libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007b46d6dbc000) +// libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007b46d6a00000) +// libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007b46d6d25000) +// /lib64/ld-linux-x86-64.so.2 (0x00007b46d6e10000) +var glibcMatcher = regexp.MustCompile(`libc.so.6`) + +func isGlibc(path string) bool { + return glibcMatcher.MatchString(path) +} + +// ❯ docker run -it --rm alpine sh -c 'ldd /bin/ls' +// +// /lib/ld-musl-x86_64.so.1 (0x71b18cdd3000) +// libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x71b18cdd3000) +var muslMatcher = regexp.MustCompile(`/lib(?:64)?/ld-musl-(.*).so.1`) + +func isMusl(path string) bool { + return muslMatcher.MatchString(path) +} + +var glibcVersionMatcher = regexp.MustCompile(`glibc 2\.(\d+)`) + +func glibcVersion(r io.ReadSeeker) (*semver.Version, error) { + matched, err := scanVersionBytes(r, glibcVersionMatcher) + if err != nil { + return nil, fmt.Errorf("scan version bytes: %w", err) + } + rawVersion := strings.TrimPrefix(string(matched), "glibc ") + v, err := semver.NewVersion(rawVersion) + if err != nil { + return nil, fmt.Errorf("parse version: %w", err) + } + nv, err := v.SetMetadata("") + if err != nil { + return nil, fmt.Errorf("set metadata: %w", err) + } + nv, err = nv.SetPrerelease("") + if err != nil { + return nil, fmt.Errorf("set prerelease: %w", err) + } + return &nv, nil +} + +var muslVersionMatcher = regexp.MustCompile(`1\.([0-9])\.(\d+)`) + +func muslVersion(r io.ReadSeeker) (*semver.Version, error) { + matched, err := scanVersionBytes(r, muslVersionMatcher) + if err != nil { + return nil, fmt.Errorf("scan version bytes: %w", err) + } + rawVersion := strings.Split(string(matched), "_")[0] + v, err := semver.NewVersion(rawVersion) + if err != nil { + return nil, fmt.Errorf("parse version: %w", err) + } + nv, err := v.SetMetadata("") + if err != nil { + return nil, fmt.Errorf("set metadata: %w", err) + } + nv, err = nv.SetPrerelease("") + if err != nil { + return nil, fmt.Errorf("set prerelease: %w", err) + } + return &nv, nil +} + +func absolutePath(proc procfs.Proc, p string) string { + return path.Join("/proc/", strconv.Itoa(proc.PID), "/root/", p) +} + +func scanVersionBytes(r io.ReadSeeker, m *regexp.Regexp) ([]byte, error) { + bufferSize := 4096 + sr, err := ainur.NewStreamReader(r, bufferSize) + if err != nil { + return nil, fmt.Errorf("failed to create stream reader: %w", err) + } + + for { + b, err := sr.Next() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("failed to read next: %w", err) + } + + matches := m.FindSubmatchIndex(b) + if matches == nil { + continue + } + + for i := 0; i < len(matches); i++ { + if matches[i] == -1 { + continue + } + + if _, err := r.Seek(int64(matches[i]), io.SeekStart); err != nil { + return nil, fmt.Errorf("failed to seek to start: %w", err) + } + + return b[matches[i]:matches[i+1]], nil + } + } + + return nil, errors.New("version not found") +} diff --git a/pkg/runtime/libc/libc_test.go b/pkg/runtime/libc/libc_test.go new file mode 100644 index 0000000000..9a9cda2f6c --- /dev/null +++ b/pkg/runtime/libc/libc_test.go @@ -0,0 +1,173 @@ +// Copyright 2022-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 libc + +import ( + "os" + "reflect" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/google/go-cmp/cmp" +) + +func Test_isGlibc(t *testing.T) { + tests := []struct { + path string + want bool + }{ + { + path: "/lib/x86_64-linux-gnu/libc.so.6", + want: true, + }, + { + path: "/lib/aarch64-linux-gnu/libc.so.6", + want: true, + }, + { + path: "/usr/lib/x86_64-linux-gnu/libc.so.6", + want: true, + }, + { + path: "/lib64/x86_64-linux-gnu/libc.so.6", + want: true, + }, + { + path: "/lib64/aarch64-linux-gnu/libc.so.6", + want: true, + }, + { + path: "aarch64-linux-gnu/libc.so.6", + want: true, + }, + { + path: "/usr/lib/libc.so.6", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + if got := isGlibc(tt.path); got != tt.want { + t.Errorf("isGlibc() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isMusl(t *testing.T) { + tests := []struct { + path string + want bool + }{ + { + path: "/lib/ld-musl-x86_64.so.1", + want: true, + }, + { + path: "/lib/ld-musl-aarch64.so.1", + want: true, + }, + { + path: "/lib64/ld-musl-x86_64.so.1", + want: true, + }, + { + path: "/lib64/ld-musl-aarch64.so.1", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + if got := isMusl(tt.path); got != tt.want { + t.Errorf("isMusl() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_glibcVersion(t *testing.T) { + tests := []struct { + name string + path string + want *semver.Version + wantErr bool + }{ + { + name: "debian", + path: "testdata/amd64/glibc.so", + want: semver.MustParse("2.36"), + }, + { + name: "debian", + path: "testdata/arm64/glibc.so", + want: semver.MustParse("2.36"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := os.Open(tt.path) + if err != nil { + t.Fatal(err) + } + got, err := glibcVersion(r) + if (err != nil) != tt.wantErr { + t.Errorf("glibcVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + return + } + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Errorf("glibcVersion() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_muslVersion(t *testing.T) { + tests := []struct { + name string + path string + want *semver.Version + wantErr bool + }{ + { + name: "alpine", + path: "testdata/amd64/musl.so", + want: semver.MustParse("1.2.4"), + }, + { + name: "alpine", + path: "testdata/arm64/musl.so", + want: semver.MustParse("1.2.4"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := os.Open(tt.path) + if err != nil { + t.Fatal(err) + } + got, err := muslVersion(r) + if (err != nil) != tt.wantErr { + t.Errorf("muslVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("muslVersion() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/runtime/libc/testdata/amd64/glibc.so b/pkg/runtime/libc/testdata/amd64/glibc.so new file mode 100755 index 0000000000..8943986406 Binary files /dev/null and b/pkg/runtime/libc/testdata/amd64/glibc.so differ diff --git a/pkg/runtime/libc/testdata/amd64/musl.so b/pkg/runtime/libc/testdata/amd64/musl.so new file mode 100755 index 0000000000..93d9fba4d0 Binary files /dev/null and b/pkg/runtime/libc/testdata/amd64/musl.so differ diff --git a/pkg/runtime/libc/testdata/arm64/glibc.so b/pkg/runtime/libc/testdata/arm64/glibc.so new file mode 100755 index 0000000000..8e679e7b9c Binary files /dev/null and b/pkg/runtime/libc/testdata/arm64/glibc.so differ diff --git a/pkg/runtime/libc/testdata/arm64/musl.so b/pkg/runtime/libc/testdata/arm64/musl.so new file mode 100755 index 0000000000..56209886cd Binary files /dev/null and b/pkg/runtime/libc/testdata/arm64/musl.so differ diff --git a/pkg/runtime/python/interpreter.go b/pkg/runtime/python/interpreter.go new file mode 100644 index 0000000000..2a2bdf4bb8 --- /dev/null +++ b/pkg/runtime/python/interpreter.go @@ -0,0 +1,389 @@ +// Copyright 2022-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 python + +import ( + "debug/elf" + "encoding/binary" + "errors" + "fmt" + "os" + goruntime "runtime" + "syscall" + "unsafe" + + "github.com/Masterminds/semver/v3" + "github.com/prometheus/procfs" + "golang.org/x/sys/unix" + + runtimedata "github.com/parca-dev/runtime-data/pkg/python" + + "github.com/parca-dev/parca-agent/pkg/elfreader" + "github.com/parca-dev/parca-agent/pkg/runtime" +) + +type interpreter struct { + exe *interpreterExecutableFile + lib *interpreterExecutableFile + + arch string + version *semver.Version +} + +func newInterpreter(proc procfs.Proc) (*interpreter, error) { + maps, err := proc.ProcMaps() + if err != nil { + return nil, fmt.Errorf("error reading process maps: %w", err) + } + + exePath, err := proc.Executable() + if err != nil { + return nil, fmt.Errorf("get executable: %w", err) + } + + isPythonBin := func(pathname string) bool { + // At this point, we know that we have a python process! + return pathname == exePath + } + + var ( + pythonExecutablePath string + pythonExecutableStartAddress uint64 + libpythonPath string + libpythonStartAddress uint64 + found bool + ) + for _, m := range maps { + if pathname := m.Pathname; pathname != "" { + if m.Perms.Execute { + if isPythonBin(pathname) { + pythonExecutablePath = pathname + pythonExecutableStartAddress = uint64(m.StartAddr) + found = true + continue + } + if isPythonLib(pathname) { + libpythonPath = pathname + libpythonStartAddress = uint64(m.StartAddr) + found = true + continue + } + } + } + } + if !found { + return nil, errors.New("not a python process") + } + + var ( + exe *interpreterExecutableFile + lib *interpreterExecutableFile + ) + if pythonExecutablePath != "" { + f, err := os.Open(absolutePath(proc, pythonExecutablePath)) + if err != nil { + return nil, fmt.Errorf("open executable: %w", err) + } + + exe, err = newInterpreterExecutableFile(proc.PID, f, pythonExecutableStartAddress) + if err != nil { + return nil, fmt.Errorf("new elf file: %w", err) + } + } + if libpythonPath != "" { + f, err := os.Open(absolutePath(proc, libpythonPath)) + if err != nil { + return nil, fmt.Errorf("open library: %w", err) + } + + lib, err = newInterpreterExecutableFile(proc.PID, f, libpythonStartAddress) + if err != nil { + return nil, fmt.Errorf("new elf file: %w", err) + } + } + + versionSources := []*interpreterExecutableFile{exe, lib} + var versionString string + for _, source := range versionSources { + if source == nil { + continue + } + + versionString, err = versionFromBSS(source) + if versionString != "" && err == nil { + break + } + } + + if versionString == "" { + for _, source := range versionSources { + if source == nil { + continue + } + + // As a last resort, try to parse the version from the path. + versionString, err = versionFromPath(source.File) + if versionString != "" && err == nil { + break + } + } + } + + version, err := semver.NewVersion(versionString) + if err != nil { + return nil, fmt.Errorf("new version: %q: %w", version, err) + } + + return &interpreter{ + exe: exe, + lib: lib, + arch: goruntime.GOARCH, + version: version, + }, nil +} + +func (i interpreter) threadStateAddress() (uint64, error) { + const37_11, err := semver.NewConstraint(">=3.7.x-0") + if err != nil { + return 0, fmt.Errorf("new constraint: %w", err) + } + + switch { + case const37_11.Check(i.version): + addr, err := i.findAddressOf(pythonRuntimeSymbol) // _PyRuntime + if err != nil { + return 0, fmt.Errorf("findAddressOf: %w", err) + } + _, initialState, err := runtimedata.GetInitialState(i.version) + if err != nil { + return 0, fmt.Errorf("get initial state: %w", err) + } + if initialState.ThreadStateCurrent < 0 { + // This version of Python does not have thread state. + // We should use TLS for the current thread state. + return 0, nil + } + return addr + uint64(initialState.ThreadStateCurrent), nil + // Older versions (<3.7.0) of Python do not have the _PyRuntime struct. + default: + addr, err := i.findAddressOf(pythonThreadStateSymbol) // _PyThreadState_Current + if err != nil { + return 0, fmt.Errorf("findAddressOf: %w", err) + } + return addr, nil + } +} + +func (i interpreter) interpreterAddress() (uint64, error) { + const37_11, err := semver.NewConstraint(">=3.7.x-0") + if err != nil { + return 0, fmt.Errorf("new constraint: %w", err) + } + + switch { + case const37_11.Check(i.version): + addr, err := i.findAddressOf(pythonRuntimeSymbol) // _PyRuntime + if err != nil { + return 0, fmt.Errorf("findAddressOf: %w", err) + } + _, initialState, err := runtimedata.GetInitialState(i.version) + if err != nil { + return 0, fmt.Errorf("get initial state: %w", err) + } + return addr + uint64(initialState.InterpreterHead), nil + // Older versions (<3.7.0) of Python do not have the _PyRuntime struct. + default: + addr, err := i.findAddressOf(pythonInterpreterSymbol) // interp_head + if err != nil { + return 0, fmt.Errorf("findAddressOf: %w", err) + } + return addr, nil + } +} + +func (i interpreter) tlsKey() (uint64, error) { + pyRuntimeAddr, err := i.findAddressOf(pythonRuntimeSymbol) // _PyRuntime + if err != nil { + return 0, fmt.Errorf("findAddressOf: %w", err) + } + _, initialState, err := runtimedata.GetInitialState(i.version) + if err != nil { + return 0, fmt.Errorf("get initial state: %w", err) + } + tssKeyAddr := pyRuntimeAddr + uint64(initialState.AutoTSSKey) + tssKeyLayout := initialState.PyTSS + + tss := make([]byte, tssKeyLayout.Size) + if err := i.copyMemory(uintptr(tssKeyAddr), tss); err != nil { + return 0, fmt.Errorf("copy memory: %w", err) + } + + // TODO(kakkoyun): offset and size of is_initialized and key should be configurable. + isInitialized := int32(binary.LittleEndian.Uint32(tss[:tssKeyLayout.Key])) + key := binary.LittleEndian.Uint32(tss[tssKeyLayout.Key:tssKeyLayout.Size]) + + if isInitialized == 0 || int(key) < 0 { + return 0, errors.New("TLS key is not initialized") + } + // TODO(kakkoyun): Use 32-bit key. + return uint64(key), nil +} + +func (i interpreter) findAddressOf(s string) (uint64, error) { + addr, err := i.exe.findAddressOf(s) + if addr != 0 && err == nil { + return addr, nil + } + + if i.lib != nil { + addr, err = i.lib.findAddressOf(s) + if addr != 0 && err == nil { + return addr, nil + } + } + + return 0, fmt.Errorf("symbol %q not found", s) +} + +func (i interpreter) copyMemory(addr uintptr, buf []byte) error { + if i.exe != nil { + if err := i.exe.copyMemory(addr, buf); err != nil { + return fmt.Errorf("copy memory from exe: %w", err) + } + } + if i.lib != nil { + if err := i.lib.copyMemory(addr, buf); err != nil { + return fmt.Errorf("copy memory from lib: %w", err) + } + } + return nil +} + +func (i interpreter) Close() error { + if i.exe != nil { + if err := i.exe.Close(); err != nil { + return fmt.Errorf("close exe: %w", err) + } + } + if i.lib != nil { + if err := i.lib.Close(); err != nil { + return fmt.Errorf("close lib: %w", err) + } + } + return nil +} + +type interpreterExecutableFile struct { + *os.File + elfFile *elf.File + + pid int + start uint64 + + cache map[string]uint64 +} + +func newInterpreterExecutableFile(pid int, f *os.File, start uint64) (*interpreterExecutableFile, error) { + ef, err := elf.NewFile(f) + if err != nil { + return nil, fmt.Errorf("new file: %w", err) + } + return &interpreterExecutableFile{ + pid: pid, + File: f, + elfFile: ef, + start: start, + cache: make(map[string]uint64), + }, nil +} + +func (ef interpreterExecutableFile) offset() uint64 { + // p_vaddr may be larger than the map address in case when the header has an offset and + // the map address is relatively small. In this case we can default to 0. + header := elfreader.FindTextProgHeader(ef.elfFile) + if header == nil { + return ef.start + } + // return ef.start - header.Vaddr + return saturatingSub(ef.start, header.Vaddr) +} + +func saturatingSub(a, b uint64) uint64 { + if b > a { + return 0 + } + return a - b +} + +type IOVec struct { + Base *byte + Len uint64 +} + +func (ef interpreterExecutableFile) copyMemory(addr uintptr, buf []byte) error { + localIOV := IOVec{ + Base: &buf[0], + Len: uint64(len(buf)), + } + remoteIOV := IOVec{ + Base: (*byte)(unsafe.Pointer(addr)), + Len: uint64(len(buf)), + } + + result, _, errno := syscall.Syscall6(unix.SYS_PROCESS_VM_READV, uintptr(ef.pid), + uintptr(unsafe.Pointer(&localIOV)), uintptr(1), + uintptr(unsafe.Pointer(&remoteIOV)), uintptr(1), + uintptr(0)) + + if result == ^uintptr(0) { // -1 in unsigned representation + //nolint:exhaustive + switch errno { + case syscall.ENOSYS, syscall.EPERM: + procMem, err := os.Open(fmt.Sprintf("/proc/%d/mem", ef.pid)) + if err != nil { + return err + } + defer procMem.Close() + + _, err = procMem.Seek(int64(addr), 0) + if err != nil { + return err + } + + _, err = procMem.Read(buf) + return err + default: + return errno + } + } + + return nil +} + +func (ef interpreterExecutableFile) findAddressOf(s string) (uint64, error) { + addr, ok := ef.cache[s] + if ok { + return addr, nil + } + // Search in both symbol and dynamic symbol tables. + symbol, err := runtime.FindSymbol(ef.elfFile, s) + if err != nil { + return 0, fmt.Errorf("FindSymbol: %w", err) + } + // Memoize the result. + addr = symbol.Value + ef.offset() + ef.cache[s] = addr + return addr, nil +} diff --git a/pkg/runtime/python/python.go b/pkg/runtime/python/python.go index 20b8de98fe..d6a0fd2d34 100644 --- a/pkg/runtime/python/python.go +++ b/pkg/runtime/python/python.go @@ -16,26 +16,17 @@ package python import ( "debug/elf" - "errors" "fmt" - "os" "path" "regexp" - goruntime "runtime" "strconv" "strings" - "syscall" - "unsafe" - - "golang.org/x/sys/unix" "github.com/Masterminds/semver/v3" "github.com/prometheus/procfs" - runtimedata "github.com/parca-dev/runtime-data/pkg/python" - - "github.com/parca-dev/parca-agent/pkg/elfreader" "github.com/parca-dev/parca-agent/pkg/runtime" + "github.com/parca-dev/parca-agent/pkg/runtime/libc" ) // Python symbols to look for: @@ -68,8 +59,15 @@ var pythonLibraryIdentifyingSymbols = [][]byte{ []byte(pythonThreadStateSymbol), } -func absolutePath(proc procfs.Proc, p string) string { - return path.Join("/proc/", strconv.Itoa(proc.PID), "/root/", p) +var libRegex = regexp.MustCompile(`/libpython\d.\d\d?(m|d|u)?.so`) + +func isPythonLib(pathname string) bool { + // Alternatively, we could check the ELF file for the interpreter symbol. + return libRegex.MatchString(pathname) +} + +func isPythonBin(pathname string) bool { + return strings.Contains(path.Base(pathname), "python") } func IsRuntime(proc procfs.Proc) (bool, error) { @@ -109,284 +107,6 @@ func IsRuntime(proc procfs.Proc) (bool, error) { return false, nil } -func versionFromBSS(f *interpreterExecutableFile) (string, error) { - ef, err := elf.NewFile(f) - if err != nil { - return "", fmt.Errorf("new file: %w", err) - } - defer ef.Close() - - for _, sec := range ef.Sections { - if sec.Name == ".bss" || sec.Type == elf.SHT_NOBITS { - if sec.Size == 0 { - continue - } - data := make([]byte, sec.Size) - if err := f.copyMemory(uintptr(f.offset()+sec.Offset), data); err != nil { - return "", fmt.Errorf("copy address: %w", err) - } - versionString, err := scanVersionBytes(data) - if err != nil { - return "", fmt.Errorf("scan version bytes: %w", err) - } - return versionString, nil - } - } - return "", errors.New("version not found") -} - -func versionFromPath(f *os.File) (string, error) { - versionString, err := scanVersionPath([]byte(f.Name())) - if err != nil { - return "", fmt.Errorf("scan version string: %w", err) - } - return versionString, nil -} - -func scanVersionBytes(data []byte) (string, error) { - re := regexp.MustCompile(`((2|3)\.(3|4|5|6|7|8|9|10|11|12)\.(\d{1,2}))((a|b|c|rc)\d{1,2})?\+? (.{1,64})`) - - match := re.FindSubmatch(data) - if match == nil { - return "", errors.New("failed to find version string") - } - - major, err := strconv.ParseUint(string(match[2]), 10, 64) - if err != nil { - return "", fmt.Errorf("parse major version: %w", err) - } - minor, err := strconv.ParseUint(string(match[3]), 10, 64) - if err != nil { - return "", fmt.Errorf("parse minor version: %w", err) - } - patch, err := strconv.ParseUint(string(match[4]), 10, 64) - if err != nil { - return "", fmt.Errorf("parse patch version: %w", err) - } - - release := "" - if len(match) > 5 && match[5] != nil { - release = string(match[5]) - } - - return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, release), nil -} - -func scanVersionPath(data []byte) (string, error) { - re := regexp.MustCompile(`python(2|3)\.(\d+)\b`) // python2.x, python3.x - - match := re.FindSubmatch(data) - if match == nil { - return "", errors.New("failed to find version string") - } - - major, err := strconv.ParseUint(string(match[1]), 10, 64) - if err != nil { - return "", fmt.Errorf("parse major version: %w", err) - } - minor, err := strconv.ParseUint(string(match[2]), 10, 64) - if err != nil { - return "", fmt.Errorf("parse minor version: %w", err) - } - - return fmt.Sprintf("%d.%d.0", major, minor), nil -} - -type interpreterExecutableFile struct { - *os.File - elfFile *elf.File - - pid int - start uint64 - - cache map[string]uint64 -} - -func newInterpreterExecutableFile(pid int, f *os.File, start uint64) (*interpreterExecutableFile, error) { - ef, err := elf.NewFile(f) - if err != nil { - return nil, fmt.Errorf("new file: %w", err) - } - return &interpreterExecutableFile{ - pid: pid, - File: f, - elfFile: ef, - start: start, - cache: make(map[string]uint64), - }, nil -} - -func (ef interpreterExecutableFile) offset() uint64 { - // p_vaddr may be larger than the map address in case when the header has an offset and - // the map address is relatively small. In this case we can default to 0. - header := elfreader.FindTextProgHeader(ef.elfFile) - if header == nil { - return ef.start - } - // return ef.start - header.Vaddr - return saturatingSub(ef.start, header.Vaddr) -} - -func saturatingSub(a, b uint64) uint64 { - if b > a { - return 0 - } - return a - b -} - -type IOVec struct { - Base *byte - Len uint64 -} - -func (ef interpreterExecutableFile) copyMemory(addr uintptr, buf []byte) error { - localIOV := IOVec{ - Base: &buf[0], - Len: uint64(len(buf)), - } - remoteIOV := IOVec{ - Base: (*byte)(unsafe.Pointer(addr)), - Len: uint64(len(buf)), - } - - result, _, errno := syscall.Syscall6(unix.SYS_PROCESS_VM_READV, uintptr(ef.pid), - uintptr(unsafe.Pointer(&localIOV)), uintptr(1), - uintptr(unsafe.Pointer(&remoteIOV)), uintptr(1), - uintptr(0)) - - if result == ^uintptr(0) { // -1 in unsigned representation - //nolint:exhaustive - switch errno { - case syscall.ENOSYS, syscall.EPERM: - procMem, err := os.Open(fmt.Sprintf("/proc/%d/mem", ef.pid)) - if err != nil { - return err - } - defer procMem.Close() - - _, err = procMem.Seek(int64(addr), 0) - if err != nil { - return err - } - - _, err = procMem.Read(buf) - return err - default: - return errno - } - } - - return nil -} - -func (ef interpreterExecutableFile) findAddressOf(s string) (uint64, error) { - addr, ok := ef.cache[s] - if ok { - return addr, nil - } - // Search in both symbol and dynamic symbol tables. - symbol, err := runtime.FindSymbol(ef.elfFile, s) - if err != nil { - return 0, fmt.Errorf("FindSymbol: %w", err) - } - // Memoize the result. - addr = symbol.Value + ef.offset() - ef.cache[s] = addr - return addr, nil -} - -type interpreter struct { - exe *interpreterExecutableFile - lib *interpreterExecutableFile - - arch string - version *semver.Version -} - -func (i interpreter) findAddressOf(s string) (uint64, error) { - addr, err := i.exe.findAddressOf(s) - if addr != 0 && err == nil { - return addr, nil - } - - if i.lib != nil { - addr, err = i.lib.findAddressOf(s) - if addr != 0 && err == nil { - return addr, nil - } - } - - return 0, fmt.Errorf("symbol %q not found", s) -} - -func (i interpreter) threadStateAddress() (uint64, error) { - const37_11, err := semver.NewConstraint(">=3.7.x") - if err != nil { - return 0, fmt.Errorf("new constraint: %w", err) - } - - switch { - case const37_11.Check(i.version): - addr, err := i.findAddressOf(pythonRuntimeSymbol) // _PyRuntime - if err != nil { - return 0, fmt.Errorf("findAddressOf: %w", err) - } - _, initialState, err := runtimedata.GetInitialState(i.version) - if err != nil { - return 0, fmt.Errorf("get initial state: %w", err) - } - return addr + uint64(initialState.ThreadStateCurrent), nil - // Older versions (<3.7.0) of Python do not have the _PyRuntime struct. - default: - addr, err := i.findAddressOf(pythonThreadStateSymbol) // _PyThreadState_Current - if err != nil { - return 0, fmt.Errorf("findAddressOf: %w", err) - } - return addr, nil - } -} - -func (i interpreter) interpreterAddress() (uint64, error) { - const37_11, err := semver.NewConstraint(">=3.7.x") - if err != nil { - return 0, fmt.Errorf("new constraint: %w", err) - } - - switch { - case const37_11.Check(i.version): - addr, err := i.findAddressOf(pythonRuntimeSymbol) // _PyRuntime - if err != nil { - return 0, fmt.Errorf("findAddressOf: %w", err) - } - _, initialState, err := runtimedata.GetInitialState(i.version) - if err != nil { - return 0, fmt.Errorf("get initial state: %w", err) - } - return addr + uint64(initialState.InterpreterHead), nil - // Older versions (<3.7.0) of Python do not have the _PyRuntime struct. - default: - addr, err := i.findAddressOf(pythonInterpreterSymbol) // interp_head - if err != nil { - return 0, fmt.Errorf("findAddressOf: %w", err) - } - return addr, nil - } -} - -func (i interpreter) Close() error { - if i.exe != nil { - if err := i.exe.Close(); err != nil { - return fmt.Errorf("close exe: %w", err) - } - } - if i.lib != nil { - if err := i.lib.Close(); err != nil { - return fmt.Errorf("close lib: %w", err) - } - } - return nil -} - func RuntimeInfo(proc procfs.Proc) (*runtime.Runtime, error) { isPython, err := IsRuntime(proc) if err != nil { @@ -408,118 +128,6 @@ func RuntimeInfo(proc procfs.Proc) (*runtime.Runtime, error) { return rt, nil } -func newInterpreter(proc procfs.Proc) (*interpreter, error) { - maps, err := proc.ProcMaps() - if err != nil { - return nil, fmt.Errorf("error reading process maps: %w", err) - } - - exePath, err := proc.Executable() - if err != nil { - return nil, fmt.Errorf("get executable: %w", err) - } - - isPythonBin := func(pathname string) bool { - // At this point, we know that we have a python process! - return pathname == exePath - } - - var ( - pythonExecutablePath string - pythonExecutableStartAddress uint64 - libpythonPath string - libpythonStartAddress uint64 - found bool - ) - for _, m := range maps { - if pathname := m.Pathname; pathname != "" { - if m.Perms.Execute { - if isPythonBin(pathname) { - pythonExecutablePath = pathname - pythonExecutableStartAddress = uint64(m.StartAddr) - found = true - continue - } - if isPythonLib(pathname) { - libpythonPath = pathname - libpythonStartAddress = uint64(m.StartAddr) - found = true - continue - } - } - } - } - if !found { - return nil, errors.New("not a python process") - } - - var ( - exe *interpreterExecutableFile - lib *interpreterExecutableFile - ) - if pythonExecutablePath != "" { - f, err := os.Open(absolutePath(proc, pythonExecutablePath)) - if err != nil { - return nil, fmt.Errorf("open executable: %w", err) - } - - exe, err = newInterpreterExecutableFile(proc.PID, f, pythonExecutableStartAddress) - if err != nil { - return nil, fmt.Errorf("new elf file: %w", err) - } - } - if libpythonPath != "" { - f, err := os.Open(absolutePath(proc, libpythonPath)) - if err != nil { - return nil, fmt.Errorf("open library: %w", err) - } - - lib, err = newInterpreterExecutableFile(proc.PID, f, libpythonStartAddress) - if err != nil { - return nil, fmt.Errorf("new elf file: %w", err) - } - } - - versionSources := []*interpreterExecutableFile{exe, lib} - var versionString string - for _, source := range versionSources { - if source == nil { - continue - } - - versionString, err = versionFromBSS(source) - if versionString != "" && err == nil { - break - } - } - - if versionString == "" { - for _, source := range versionSources { - if source == nil { - continue - } - - // As a last resort, try to parse the version from the path. - versionString, err = versionFromPath(source.File) - if versionString != "" && err == nil { - break - } - } - } - - version, err := semver.NewVersion(versionString) - if err != nil { - return nil, fmt.Errorf("new version: %q: %w", version, err) - } - - return &interpreter{ - exe: exe, - lib: lib, - arch: goruntime.GOARCH, - version: version, - }, nil -} - func InterpreterInfo(proc procfs.Proc) (*runtime.Interpreter, error) { interpreter, err := newInterpreter(proc) if err != nil { @@ -531,8 +139,13 @@ func InterpreterInfo(proc procfs.Proc) (*runtime.Interpreter, error) { if err != nil { return nil, fmt.Errorf("python version: %s, thread state address: %w", interpreter.version.String(), err) } + + var tlsKey uint64 if threadStateAddress == 0 { - return nil, fmt.Errorf("invalid address, python version: %s, thread state address: 0x%016x", interpreter.version.String(), threadStateAddress) + tlsKey, err = interpreter.tlsKey() + if err != nil { + return nil, fmt.Errorf("python version: %s, tls key: %w", interpreter.version.String(), err) + } } interpreterAddress, err := interpreter.interpreterAddress() @@ -543,24 +156,31 @@ func InterpreterInfo(proc procfs.Proc) (*runtime.Interpreter, error) { return nil, fmt.Errorf("invalid address, python version: %s, interpreter address: 0x%016x", interpreter.version.String(), interpreterAddress) } + libcInfo, err := libc.NewLibcInfo(proc) + if err != nil { + above312, err := semver.NewConstraint(">=3.12.0-0") + if err != nil { + return nil, fmt.Errorf("python version: %s, libc info: %w", interpreter.version.String(), err) + } + if above312.Check(interpreter.version) { + // It's only critical to have the libc info for Python 3.12 and above. + return nil, fmt.Errorf("python version: %s, libc info: %w", interpreter.version.String(), err) + } + } + return &runtime.Interpreter{ Runtime: runtime.Runtime{ Name: "Python", Version: interpreter.version.String(), }, Type: runtime.InterpreterPython, + LibcInfo: libcInfo, MainThreadAddress: threadStateAddress, + TLSKey: tlsKey, InterpreterAddress: interpreterAddress, }, nil } -var libRegex = regexp.MustCompile(`/libpython\d.\d\d?(m|d|u)?.so`) - -func isPythonLib(pathname string) bool { - // Alternatively, we could check the ELF file for the interpreter symbol. - return libRegex.MatchString(pathname) -} - -func isPythonBin(pathname string) bool { - return strings.Contains(path.Base(pathname), "python") +func absolutePath(proc procfs.Proc, p string) string { + return path.Join("/proc/", strconv.Itoa(proc.PID), "/root/", p) } diff --git a/pkg/runtime/python/version.go b/pkg/runtime/python/version.go new file mode 100644 index 0000000000..191df862cb --- /dev/null +++ b/pkg/runtime/python/version.go @@ -0,0 +1,107 @@ +// Copyright 2022-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 python + +import ( + "debug/elf" + "errors" + "fmt" + "os" + "regexp" + "strconv" +) + +func versionFromBSS(f *interpreterExecutableFile) (string, error) { + ef, err := elf.NewFile(f) + if err != nil { + return "", fmt.Errorf("new file: %w", err) + } + defer ef.Close() + + for _, sec := range ef.Sections { + if sec.Name == ".bss" || sec.Type == elf.SHT_NOBITS { + if sec.Size == 0 { + continue + } + data := make([]byte, sec.Size) + if err := f.copyMemory(uintptr(f.offset()+sec.Offset), data); err != nil { + return "", fmt.Errorf("copy address: %w", err) + } + versionString, err := scanVersionBytes(data) + if err != nil { + return "", fmt.Errorf("scan version bytes: %w", err) + } + return versionString, nil + } + } + return "", errors.New("version not found") +} + +func versionFromPath(f *os.File) (string, error) { + versionString, err := scanVersionPath([]byte(f.Name())) + if err != nil { + return "", fmt.Errorf("scan version string: %w", err) + } + return versionString, nil +} + +func scanVersionBytes(data []byte) (string, error) { + re := regexp.MustCompile(`((2|3)\.(3|4|5|6|7|8|9|10|11|12)\.(\d{1,2}))((a|b|c|rc)\d{1,2})?\+? (.{1,64})`) + + match := re.FindSubmatch(data) + if match == nil { + return "", errors.New("failed to find version string") + } + + major, err := strconv.ParseUint(string(match[2]), 10, 64) + if err != nil { + return "", fmt.Errorf("parse major version: %w", err) + } + minor, err := strconv.ParseUint(string(match[3]), 10, 64) + if err != nil { + return "", fmt.Errorf("parse minor version: %w", err) + } + patch, err := strconv.ParseUint(string(match[4]), 10, 64) + if err != nil { + return "", fmt.Errorf("parse patch version: %w", err) + } + + release := "" + if len(match) > 5 && match[5] != nil { + release = string(match[5]) + } + + return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, release), nil +} + +func scanVersionPath(data []byte) (string, error) { + re := regexp.MustCompile(`python(2|3)\.(\d+)\b`) // python2.x, python3.x + + match := re.FindSubmatch(data) + if match == nil { + return "", errors.New("failed to find version string") + } + + major, err := strconv.ParseUint(string(match[1]), 10, 64) + if err != nil { + return "", fmt.Errorf("parse major version: %w", err) + } + minor, err := strconv.ParseUint(string(match[2]), 10, 64) + if err != nil { + return "", fmt.Errorf("parse minor version: %w", err) + } + + return fmt.Sprintf("%d.%d.0", major, minor), nil +} diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 9616c555f3..b0ca4ccb4c 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -13,6 +13,8 @@ package runtime +import "github.com/parca-dev/parca-agent/pkg/runtime/libc" + type InterpreterType uint64 const ( @@ -45,7 +47,10 @@ type Interpreter struct { Runtime Type InterpreterType - // The address of the main thread state for Python. + LibcInfo *libc.LibcInfo + + // The address of the main thread state for interpreters. MainThreadAddress uint64 InterpreterAddress uint64 + TLSKey uint64 } diff --git a/test/integration/python/python_test.go b/test/integration/python/python_test.go index d9b9cf5394..e919228132 100644 --- a/test/integration/python/python_test.go +++ b/test/integration/python/python_test.go @@ -17,6 +17,7 @@ package python import ( "context" "fmt" + "strings" "testing" "time" @@ -41,23 +42,57 @@ func TestPython(t *testing.T) { } tests := []struct { - images map[string]string - program string - want []string - wantErr bool + versionImages map[string][]string + program string + want []string + wantErr bool }{ { - images: map[string]string{ - "2.7": "2.7.18-slim", - "3.3": "3.3.7-slim", - "3.4": "3.4.8-slim", - "3.5": "3.5.5-slim", - "3.6": "3.6.6-slim", - "3.7": "3.7.0-slim", - "3.8": "3.8.0-slim", - "3.9": "3.9.5-slim", - "3.10": "3.10.0-slim", - "3.11": "3.11.0-slim", + versionImages: map[string][]string{ + "2.7": { + "2.7.18-slim", + "2.7.18-alpine", + }, + "3.3": { + "3.3.7-slim", + "3.3.7-alpine", + }, + "3.4": { + "3.4.8-slim", + "3.4.8-alpine", + }, + "3.5": { + "3.5.5-slim", + "3.5.5-alpine", + }, + "3.6": { + "3.6.6-slim", + "3.6.6-alpine", + }, + "3.7": { + "3.7.0-slim", + "3.7.0-alpine", + }, + "3.8": { + "3.8.0-slim", + "3.8.0-alpine", + }, + "3.9": { + "3.9.5-slim", + "3.9.5-alpine", + }, + "3.10": { + "3.10.0-slim", + "3.10.0-alpine", + }, + "3.11": { + "3.11.0-slim", + "3.11.0-alpine", + }, + "3.12": { + "3.12.2-slim", + "3.12.2-alpine", + }, }, program: "testdata/cpu_hog.py", want: []string{"", "a1", "b1", "c1", "cpu"}, @@ -65,119 +100,126 @@ func TestPython(t *testing.T) { }, } for _, tt := range tests { - for version, imageTag := range tt.images { - var ( - program = tt.program - want = tt.want - name = fmt.Sprintf("%s on python-%s", imageTag, program) - version = version - ) - t.Run(name, func(t *testing.T) { - // Start a python container. - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - python, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: testcontainers.ContainerRequest{ - Image: fmt.Sprintf("python:%s", imageTag), - Files: []testcontainers.ContainerFile{ - { - HostFilePath: program, - ContainerFilePath: "/test.py", - FileMode: 0o700, + for version, imageTags := range tt.versionImages { + for _, imageTag := range imageTags { + if strings.Contains(imageTag, "alpine") { + // Skip alpine images until https://github.com/parca-dev/parca-agent/issues/1658 is resolved. + t.Logf("skipping alpine images") + continue + } + var ( + program = tt.program + want = tt.want + name = fmt.Sprintf("%s on python-%s", imageTag, program) + version = version + ) + t.Run(name, func(t *testing.T) { + // Start a python container. + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + python, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("python:%s", imageTag), + Files: []testcontainers.ContainerFile{ + { + HostFilePath: program, + ContainerFilePath: "/test.py", + FileMode: 0o700, + }, }, + Cmd: []string{"python", "/test.py"}, }, - Cmd: []string{"python", "/test.py"}, - }, - Started: true, - }) - require.NoError(t, err) + Started: true, + }) + require.NoError(t, err) - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() - err := python.Terminate(ctx) - if err != nil { - require.ErrorIs(t, err, context.DeadlineExceeded) - } - }) - - state, err := python.State(ctx) - require.NoError(t, err) - - if !state.Running { - t.Logf("python (%s) is not running", name) - } - - t.Logf("python (%s) is running with pid %d", version, state.Pid) + err := python.Terminate(ctx) + if err != nil { + require.ErrorIs(t, err, context.DeadlineExceeded) + } + }) - // Start the agent. - var ( - profileStore = integration.NewTestProfileStore() - profileDuration = integration.ProfileDuration() + state, err := python.State(ctx) + require.NoError(t, err) - logger = logger.NewLogger("error", logger.LogFormatLogfmt, "parca-agent-tests") - reg = prometheus.NewRegistry() - ofp = objectfile.NewPool(logger, reg, "", 10, 0) - ) - t.Cleanup(func() { - ofp.Close() - }) + if !state.Running { + t.Logf("python (%s) is not running", name) + } - profiler, err := integration.NewTestProfiler(logger, reg, ofp, profileStore, t.TempDir(), &cpu.Config{ - ProfilingDuration: 1 * time.Second, - ProfilingSamplingFrequency: uint64(27), - PerfEventBufferPollInterval: 250, - PerfEventBufferProcessingInterval: 100, - PerfEventBufferWorkerCount: 8, - MemlockRlimit: uint64(4000000), - DebugProcessNames: []string{}, - DWARFUnwindingDisabled: false, - DWARFUnwindingMixedModeEnabled: false, - PythonUnwindingEnabled: true, - RubyUnwindingEnabled: false, - BPFVerboseLoggingEnabled: false, // Enable for debugging. - BPFEventsBufferSize: 8192, - RateLimitUnwindInfo: 50, - RateLimitProcessMappings: 50, - RateLimitRefreshProcessInfo: 50, - }, - &relabel.Config{ - Action: relabel.Keep, - SourceLabels: model.LabelNames{"python"}, - Regex: relabel.MustNewRegexp("true"), - }, - &relabel.Config{ - Action: relabel.Keep, - SourceLabels: model.LabelNames{"python_version"}, - Regex: relabel.MustNewRegexp(fmt.Sprintf("%s.*", version)), + t.Logf("python (%s) is running with pid %d", version, state.Pid) + + // Start the agent. + var ( + profileStore = integration.NewTestProfileStore() + profileDuration = integration.ProfileDuration() + + logger = logger.NewLogger("error", logger.LogFormatLogfmt, "parca-agent-tests") + reg = prometheus.NewRegistry() + ofp = objectfile.NewPool(logger, reg, "", 10, 0) + ) + t.Cleanup(func() { + ofp.Close() + }) + + profiler, err := integration.NewTestProfiler(logger, reg, ofp, profileStore, t.TempDir(), &cpu.Config{ + ProfilingDuration: 1 * time.Second, + ProfilingSamplingFrequency: uint64(27), + PerfEventBufferPollInterval: 250, + PerfEventBufferProcessingInterval: 100, + PerfEventBufferWorkerCount: 8, + MemlockRlimit: uint64(4000000), + DebugProcessNames: []string{}, + DWARFUnwindingDisabled: false, + DWARFUnwindingMixedModeEnabled: false, + PythonUnwindingEnabled: true, + RubyUnwindingEnabled: false, + BPFVerboseLoggingEnabled: false, // Enable for debugging. + BPFEventsBufferSize: 8192, + RateLimitUnwindInfo: 50, + RateLimitProcessMappings: 50, + RateLimitRefreshProcessInfo: 50, }, - ) - require.NoError(t, err) + &relabel.Config{ + Action: relabel.Keep, + SourceLabels: model.LabelNames{"python"}, + Regex: relabel.MustNewRegexp("true"), + }, + &relabel.Config{ + Action: relabel.Keep, + SourceLabels: model.LabelNames{"python_version"}, + Regex: relabel.MustNewRegexp(fmt.Sprintf("%s.*", version)), + }, + ) + require.NoError(t, err) - ctx, cancel = context.WithTimeout(context.Background(), profileDuration) - t.Cleanup(cancel) + ctx, cancel = context.WithTimeout(context.Background(), profileDuration) + t.Cleanup(cancel) - require.Equal(t, profiler.Run(ctx), context.DeadlineExceeded) - require.NotEmpty(t, profileStore.Samples) + require.Equal(t, profiler.Run(ctx), context.DeadlineExceeded) + require.NotEmpty(t, profileStore.Samples) - sample := profileStore.SampleForProcess(state.Pid, false) - require.NotNil(t, sample) + sample := profileStore.SampleForProcess(state.Pid, false) + require.NotNil(t, sample) - require.Less(t, sample.Profile.DurationNanos, profileDuration.Nanoseconds()) - require.Equal(t, "samples", sample.Profile.SampleType[0].Type) - require.Equal(t, "count", sample.Profile.SampleType[0].Unit) + require.Less(t, sample.Profile.DurationNanos, profileDuration.Nanoseconds()) + require.Equal(t, "samples", sample.Profile.SampleType[0].Type) + require.Equal(t, "count", sample.Profile.SampleType[0].Unit) - require.NotEmpty(t, sample.Profile.Sample) - require.NotEmpty(t, sample.Profile.Location) - require.NotEmpty(t, sample.Profile.Mapping) + require.NotEmpty(t, sample.Profile.Sample) + require.NotEmpty(t, sample.Profile.Location) + require.NotEmpty(t, sample.Profile.Mapping) - aggregatedStack, err := integration.AggregateStacks(sample.Profile) - require.NoError(t, err) + aggregatedStack, err := integration.AggregateStacks(sample.Profile) + require.NoError(t, err) - integration.RequireAnyStackContains(t, aggregatedStack, want) - }) + integration.RequireAnyStackContains(t, aggregatedStack, want) + }) + } } } }