Skip to content

Commit

Permalink
feat: add support for CPython 3.13
Browse files Browse the repository at this point in the history
We add support for CPython 3.13
  • Loading branch information
P403n1x87 committed Jul 5, 2024
1 parent 4559915 commit 660c19d
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 58 deletions.
9 changes: 3 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +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 -E env PATH="$PATH" .venv/bin/pytest --pastebin=failed -svr a test/functional -k "not austinp"
sudo -E env PATH="$PATH" -s 'echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern && ulimit -c unlimited && .venv/bin/pytest --pastebin=failed -svr a test/functional -k "not austinp"'
if: always()

- name: Run functional Austin tests (without sudo)
Expand All @@ -145,9 +144,8 @@ jobs:

- name: Run functional austinp tests (with sudo)
run: |
ulimit -c unlimited
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 "austinp"
sudo -E env PATH="$PATH" -s 'ulimit -c unlimited && .venv/bin/pytest --pastebin=failed -svr a test/functional -k "austinp"'
if: always()

- name: Run functional austinp tests (without sudo)
Expand All @@ -159,9 +157,8 @@ jobs:

- name: Run integrity tests (with sudo)
run: |
ulimit -c unlimited
echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern
sudo -E env PATH="$PATH" .venv/bin/pytest --pastebin=failed -svr a test/integrity
sudo -E env PATH="$PATH" -s "ulimit -c unlimited && .venv/bin/pytest --pastebin=failed -svr a test/integrity"
if: always()

- name: Run integrity tests (without sudo)
Expand Down
88 changes: 73 additions & 15 deletions src/py_proc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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))) {
Expand Down Expand Up @@ -560,6 +581,39 @@ _py_proc__get_current_thread_state_raddr(py_proc_t * self) {
return (void *) -1;
}

// ----------------------------------------------------------------------------
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);

sfree(self->is);
sfree(self->ts);

FAIL;
}

// ----------------------------------------------------------------------------
static int
_py_proc__find_interpreter_state(py_proc_t * self) {
Expand All @@ -575,6 +629,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

Expand Down Expand Up @@ -703,6 +760,9 @@ py_proc_new(int child) {

py_proc->child = child;
py_proc->gc_state_raddr = NULL;

py_proc->is = NULL;
py_proc->ts = NULL;

_prehash_symbols();

Expand Down Expand Up @@ -1200,15 +1260,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.
Expand All @@ -1223,7 +1281,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))) {
Expand All @@ -1234,7 +1292,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();
Expand Down
17 changes: 17 additions & 0 deletions src/py_proc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 9 additions & 9 deletions src/py_thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand All @@ -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)));
}


Expand Down Expand Up @@ -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;
Expand Down
105 changes: 105 additions & 0 deletions src/python/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 660c19d

Please sign in to comment.