diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8931f7f..5204f3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -132,7 +132,7 @@ jobs: - name: Run functional Austin tests (with sudo) run: | ulimit -c unlimited - echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern + sudo echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern sudo -E env PATH="$PATH" .venv/bin/pytest --pastebin=failed -svr a test/functional -k "not austinp" if: always() diff --git a/src/py_proc.c b/src/py_proc.c index 9951dd6..e0f6663 100644 --- a/src/py_proc.c +++ b/src/py_proc.c @@ -235,6 +235,30 @@ _py_proc__infer_python_version(py_proc_t * self) { int major = 0, minor = 0, patch = 0; + // Starting with Python 3.13 we can use the PyRuntime structure + if (isvalid(self->symbols[DYNSYM_RUNTIME])) { + _Py_DebugOffsets py_d; + if (fail(py_proc__get_type(self, self->symbols[DYNSYM_RUNTIME], py_d))) { + log_e("Cannot copy PyRuntimeState structure from remote address"); + FAIL; + } + + if (0 == memcmp(py_d.v3_13.cookie, "xdebugpy", sizeof(py_d.v3_13.cookie))) { + uint64_t version = py_d.v3_13.version; + major = (version>>24) & 0xFF; + minor = (version>>16) & 0xFF; + patch = (version>>8) & 0xFF; + + log_d("Python version (from debug offsets): %d.%d.%d", major, minor, patch); + + self->py_v = get_version_descriptor(major, minor, patch); + + init_version_descriptor(self->py_v, &py_d); + + SUCCESS; + } + } + // Starting with Python 3.11 we can rely on the Py_Version symbol if (isvalid(self->symbols[DYNSYM_HEX_VERSION])) { unsigned long py_version = 0; @@ -322,17 +346,14 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) { V_DESC(self->py_v); - PyInterpreterState is; - PyThreadState tstate_head; - - if (py_proc__get_type(self, raddr, is)) { + if (py_proc__copy_v(self, is, raddr, self->is)) { log_ie("Cannot get remote interpreter state"); FAIL; } + log_d("Interpreter state buffer %p", self->is); + void * tstate_head_addr = V_FIELD_PTR(void *, self->is, py_is, o_tstate_head); - void * tstate_head_addr = V_FIELD(void *, is, py_is, o_tstate_head); - - if (fail(py_proc__get_type(self, tstate_head_addr, tstate_head))) { + if (fail(py_proc__copy_v(self, thread, tstate_head_addr, self->ts))) { log_e( "Cannot copy PyThreadState head at %p from PyInterpreterState instance", tstate_head_addr @@ -342,7 +363,7 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) { log_t("PyThreadState head loaded @ %p", V_FIELD(void *, is, py_is, o_tstate_head)); - if (V_FIELD(void*, tstate_head, py_thread, o_interp) != raddr) { + if (V_FIELD_PTR(void*, self->ts, py_thread, o_interp) != raddr) { log_d("PyThreadState head does not point to interpreter state"); set_error(EPROC); FAIL; @@ -358,7 +379,7 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) { raddr, V_FIELD(void *, is, py_is, o_tstate_head) ); - raddr_t thread_raddr = {self->proc_ref, V_FIELD(void *, is, py_is, o_tstate_head)}; + raddr_t thread_raddr = {self->proc_ref, V_FIELD_PTR(void *, self->is, py_is, o_tstate_head)}; py_thread_t thread; if (fail(py_thread__fill_from_raddr(&thread, &thread_raddr, self))) { @@ -560,6 +581,45 @@ _py_proc__get_current_thread_state_raddr(py_proc_t * self) { return (void *) -1; } +// ---------------------------------------------------------------------------- +static void +_py_proc__free_local_buffers(py_proc_t * self) { + sfree(self->is); + sfree(self->ts); +} + +// ---------------------------------------------------------------------------- +static int +_py_proc__init_local_buffers(py_proc_t * self) { + if (!isvalid(self)) { + set_error(EPROC); + FAIL; + } + + self->is = calloc(1, self->py_v->py_is.size); + if (!isvalid(self->is)) { + log_e("Cannot allocate memory for PyInterpreterState"); + goto error; + } + + self->ts = calloc(1, self->py_v->py_thread.size); + if (!isvalid(self->ts)) { + log_e("Cannot allocate memory for PyThreadState"); + goto error; + } + + log_d("Local buffers initialised"); + + SUCCESS; + +error: + set_error(ENOMEM); + + _py_proc__free_local_buffers(self); + + FAIL; +} + // ---------------------------------------------------------------------------- static int _py_proc__find_interpreter_state(py_proc_t * self) { @@ -575,6 +635,9 @@ _py_proc__find_interpreter_state(py_proc_t * self) { if (fail(_py_proc__infer_python_version(self))) FAIL; + if (fail(_py_proc__init_local_buffers(self))) + FAIL; + if (self->sym_loaded || isvalid(self->map.runtime.base)) { // Try to resolve the symbols or the runtime section, if we have them @@ -703,6 +766,10 @@ py_proc_new(int child) { py_proc->child = child; py_proc->gc_state_raddr = NULL; + py_proc->py_v = NULL; + + py_proc->is = NULL; + py_proc->ts = NULL; _prehash_symbols(); @@ -1200,15 +1267,13 @@ py_proc__sample(py_proc_t * self) { V_DESC(self->py_v); - PyInterpreterState is; - do { - if (fail(py_proc__get_type(self, current_interp, is))) { + if (fail(py_proc__copy_v(self, is, current_interp, self->is))) { log_ie("Failed to get interpreter state while sampling"); FAIL; } - void * tstate_head = V_FIELD(void *, is, py_is, o_tstate_head); + void * tstate_head = V_FIELD_PTR(void *, self->is, py_is, o_tstate_head); if (!isvalid(tstate_head)) // Maybe the interpreter state is in an invalid state. We'll try again // unless there is a fatal error. @@ -1223,7 +1288,7 @@ py_proc__sample(py_proc_t * self) { time_delta = gettime() - self->timestamp; #endif - int result = _py_proc__sample_interpreter(self, &is, time_delta); + int result = _py_proc__sample_interpreter(self, self->is, time_delta); #ifdef NATIVE if (fail(_py_proc__resume_threads(self, &raddr))) { @@ -1234,7 +1299,7 @@ py_proc__sample(py_proc_t * self) { if (fail(result)) FAIL; - } while (isvalid(current_interp = V_FIELD(void *, is, py_is, o_next))); + } while (isvalid(current_interp = V_FIELD_PTR(void *, self->is, py_is, o_next))); #ifdef NATIVE self->timestamp = gettime(); @@ -1321,6 +1386,8 @@ py_proc__destroy(py_proc_t * self) { hash_table__destroy(self->base_table); #endif + _py_proc__free_local_buffers(self); + sfree(self->bin_path); sfree(self->lib_path); sfree(self->extra); diff --git a/src/py_proc.h b/src/py_proc.h index 56afdd7..a5d05ef 100644 --- a/src/py_proc.h +++ b/src/py_proc.h @@ -99,6 +99,10 @@ typedef struct { hash_table_t * base_table; #endif + // Local buffers + PyInterpreterState * is; + PyThreadState * ts; + // Platform-dependent fields proc_extra_info * extra; } py_proc_t; @@ -208,6 +212,19 @@ py_proc__sample(py_proc_t *); */ #define py_proc__get_type(self, raddr, dt) (py_proc__memcpy(self, raddr, sizeof(dt), &dt)) +/** + * Make a local copy of a remote structure. + * + * @param self the process object. + * @param type the type of the structure. + * @param raddr the remote address of the structure. + * @param dest the destination address. + * + * @return 0 on success. + */ +#define py_proc__copy_v(self, type, raddr, dest) (py_proc__memcpy(self, raddr, py_v->py_##type.size, dest)) + + /** * Log the Python interpreter version * @param self the process object. diff --git a/src/py_proc_list.c b/src/py_proc_list.c index 7fc682a..88fcbb2 100644 --- a/src/py_proc_list.c +++ b/src/py_proc_list.c @@ -176,7 +176,7 @@ py_proc_list__sample(py_proc_list_t * self) { for (py_proc_item_t * item = self->first; item != NULL; /* item = item->next */) { log_t("Sampling process with PID %d", item->py_proc->pid); stopwatch_start(); - if (fail(py_proc__sample(item->py_proc))) { + if (!isvalid(item->py_proc->py_v) || fail(py_proc__sample(item->py_proc))) { py_proc__wait(item->py_proc); py_proc_item_t * next = item->next; _py_proc_list__remove(self, item); diff --git a/src/py_thread.c b/src/py_thread.c index 196e997..3c43bf2 100644 --- a/src/py_thread.c +++ b/src/py_thread.c @@ -424,7 +424,7 @@ static inline int _py_thread__unwind_iframe_stack(py_thread_t * self, void * iframe_raddr) { int invalid = FALSE; void * curr = iframe_raddr; - + while (isvalid(curr)) { if (fail(_py_thread__push_iframe(self, &curr))) { log_d("Failed to retrieve iframe #%d", stack_pointer()); @@ -457,8 +457,6 @@ static inline int _py_thread__unwind_cframe_stack(py_thread_t * self) { PyCFrame cframe; - int invalid = FALSE; - _py_thread__read_stack(self); stack_reset(); @@ -470,11 +468,7 @@ _py_thread__unwind_cframe_stack(py_thread_t * self) { FAIL; } - invalid = fail(_py_thread__unwind_iframe_stack(self, V_FIELD(void *, cframe, py_cframe, o_current_frame))); - if (invalid) - return invalid; - - return invalid; + return fail(_py_thread__unwind_iframe_stack(self, V_FIELD(void *, cframe, py_cframe, o_current_frame))); } @@ -954,7 +948,13 @@ py_thread__emit_collapsed_stack(py_thread_t * self, int64_t interp_id, ctime_t t V_DESC(self->proc->py_v); if (isvalid(self->top_frame)) { - if (V_MIN(3, 11)) { + if (V_MIN(3, 13)) { + if (fail(_py_thread__unwind_iframe_stack(self, self->top_frame))) { + emit_invalid_frame(); + error = TRUE; + } + } + else if (V_MIN(3, 11)) { if (fail(_py_thread__unwind_cframe_stack(self))) { emit_invalid_frame(); error = TRUE; diff --git a/src/python/runtime.h b/src/python/runtime.h index 9b4147a..41e2d0d 100644 --- a/src/python/runtime.h +++ b/src/python/runtime.h @@ -156,4 +156,109 @@ typedef union { _PyRuntimeState3_12 v3_12; } _PyRuntimeState; + +// Starting with CPython 3.13, we can retrieve the offsets from a dedicated +// data structure + +typedef struct _Py_DebugOffsets3_13 { + char cookie[8]; // xdebugpy + uint64_t version; + // Runtime state offset; + struct _runtime_state { + uint64_t size; + uint64_t finalizing; + uint64_t interpreters_head; + } runtime_state; + + // Interpreter state offset; + struct _interpreter_state { + uint64_t size; + uint64_t id; + uint64_t next; + uint64_t threads_head; + uint64_t gc; + uint64_t imports_modules; + uint64_t sysdict; + uint64_t builtins; + uint64_t ceval_gil; + uint64_t gil_runtime_state_locked; + uint64_t gil_runtime_state_holder; + } interpreter_state; + + // Thread state offset; + struct _thread_state{ + uint64_t size; + uint64_t prev; + uint64_t next; + uint64_t interp; + uint64_t current_frame; + uint64_t thread_id; + uint64_t native_thread_id; + uint64_t datastack_chunk; + uint64_t status; + } thread_state; + + // InterpreterFrame offset; + struct _interpreter_frame { + uint64_t size; + uint64_t previous; + uint64_t executable; + uint64_t instr_ptr; + uint64_t localsplus; + uint64_t owner; + } interpreter_frame; + + // Code object offset; + struct _code_object { + uint64_t size; + uint64_t filename; + uint64_t name; + uint64_t qualname; + uint64_t linetable; + uint64_t firstlineno; + uint64_t argcount; + uint64_t localsplusnames; + uint64_t localspluskinds; + uint64_t co_code_adaptive; + } code_object; + + // PyObject offset; + struct _pyobject { + uint64_t size; + uint64_t ob_type; + } pyobject; + + // PyTypeObject object offset; + struct _type_object { + uint64_t size; + uint64_t tp_name; + } type_object; + + // PyTuple object offset; + struct _tuple_object { + uint64_t size; + uint64_t ob_item; + } tuple_object; + + // Unicode object offset; + struct _unicode_object { + uint64_t size; + uint64_t state; + uint64_t length; + size_t asciiobject_size; + } unicode_object; + + // GC runtime state offset; + struct _gc { + uint64_t size; + uint64_t collecting; + } gc; +} _Py_DebugOffsets3_13; + + +typedef union { + _Py_DebugOffsets3_13 v3_13; +} _Py_DebugOffsets; + + #endif diff --git a/src/python/string.h b/src/python/string.h index 8d768ff..c1a2c90 100644 --- a/src/python/string.h +++ b/src/python/string.h @@ -120,14 +120,4 @@ typedef struct { char ob_sval[1]; } PyBytesObject; - -// ---- stringobject.h -------------------------------------------------------- - -typedef struct { - PyObject_VAR_HEAD - long ob_shash; - int ob_sstate; - char ob_sval[1]; -} PyStringObject; /* From Python 2.7 */ - #endif diff --git a/src/version.h b/src/version.h index c415336..d6aefb6 100644 --- a/src/version.h +++ b/src/version.h @@ -140,16 +140,6 @@ typedef struct { } py_thread_v; -typedef struct { - int version; -} py_unicode_v; - - -typedef struct { - int version; -} py_bytes_v; - - typedef struct { ssize_t size; @@ -280,14 +270,6 @@ typedef struct { offsetof(s, _status) \ } -#define PY_UNICODE(n) { \ - n \ -} - -#define PY_BYTES(n) { \ - n \ -} - #define PY_RUNTIME(s) { \ sizeof(s), \ offsetof(s, interpreters.head), \ @@ -390,6 +372,10 @@ python_v python_v3_12 = { PY_IFRAME_312 (_PyInterpreterFrame3_12), }; +// ---- Python 3.13 ----------------------------------------------------------- + +python_v python_v3_13; + // ---------------------------------------------------------------------------- static inline python_v * get_version_descriptor(int major, int minor, int patch) { @@ -428,6 +414,9 @@ get_version_descriptor(int major, int minor, int patch) { // 3.12 case 12: py_v = &python_v3_12; break; + // 3.13+ + case 13: py_v = &python_v3_13; break; + default: py_v = LATEST_VERSION; UNSUPPORTED_VERSION; } @@ -440,6 +429,74 @@ get_version_descriptor(int major, int minor, int patch) { return py_v; } +// ---------------------------------------------------------------------------- + +#define V_ASSIGN(ver, dst, src) {py_v->py_##dst = py_d->v##ver.src;log_d("py_%s = %ld", #dst, py_v->py_##dst);} + +#define PY_CODE_313(v) { \ + V_ASSIGN(v, code.size, code_object.size) \ + V_ASSIGN(v, code.o_filename, code_object.filename) \ + V_ASSIGN(v, code.o_name, code_object.name) \ + V_ASSIGN(v, code.o_lnotab, code_object.linetable) \ + V_ASSIGN(v, code.o_firstlineno, code_object.firstlineno) \ + V_ASSIGN(v, code.o_code, code_object.co_code_adaptive) \ + V_ASSIGN(v, code.o_qualname, code_object.qualname) \ +} + +#define PY_IFRAME_313(v) { \ + V_ASSIGN(v, iframe.size, interpreter_frame.size) \ + V_ASSIGN(v, iframe.o_previous, interpreter_frame.previous) \ + V_ASSIGN(v, iframe.o_code, interpreter_frame.executable) \ + V_ASSIGN(v, iframe.o_prev_instr, interpreter_frame.instr_ptr) \ + V_ASSIGN(v, iframe.o_owner, interpreter_frame.owner) \ +} + +#define PY_THREAD_313(v) { \ + V_ASSIGN(v, thread.size, thread_state.size) \ + V_ASSIGN(v, thread.o_prev, thread_state.prev) \ + V_ASSIGN(v, thread.o_next, thread_state.next) \ + V_ASSIGN(v, thread.o_interp, thread_state.interp) \ + V_ASSIGN(v, thread.o_frame, thread_state.current_frame) \ + V_ASSIGN(v, thread.o_thread_id, thread_state.thread_id) \ + V_ASSIGN(v, thread.o_native_thread_id, thread_state.native_thread_id) \ + V_ASSIGN(v, thread.o_stack, thread_state.datastack_chunk) \ + V_ASSIGN(v, thread.o_status, thread_state.status) \ +} + +#define PY_RUNTIME_313(v) { \ + V_ASSIGN(v, runtime.size, runtime_state.size) \ + V_ASSIGN(v, runtime.o_interp_head, runtime_state.interpreters_head) \ +} + +#define PY_IS_313(v) { \ + V_ASSIGN(v, is.size, interpreter_state.size) \ + V_ASSIGN(v, is.o_next, interpreter_state.next) \ + V_ASSIGN(v, is.o_tstate_head, interpreter_state.threads_head) \ + V_ASSIGN(v, is.o_id, interpreter_state.id) \ + V_ASSIGN(v, is.o_gc, interpreter_state.gc) \ + V_ASSIGN(v, is.o_gil_state, interpreter_state.ceval_gil) \ +} + +#define PY_GC_313(v) { \ + V_ASSIGN(v, gc.size, gc.size) \ + V_ASSIGN(v, gc.o_collecting, gc.collecting) \ +} + +// ---------------------------------------------------------------------------- +static void +init_version_descriptor(python_v * py_v, _Py_DebugOffsets* py_d) { + switch (py_v->minor) { + case 13: + PY_CODE_313(3_13); + PY_IFRAME_313(3_13); + PY_THREAD_313(3_13); + PY_RUNTIME_313(3_13); + PY_IS_313(3_13); + PY_GC_313(3_13); + } +} + + #endif // PY_PROC_C #endif