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 e66cdb1
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
97 changes: 82 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;

Check warning on line 250 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L247-L250

Added lines #L247 - L250 were not covered by tests

log_d("Python version (from debug offsets): %d.%d.%d", major, minor, patch);

self->py_v = get_version_descriptor(major, minor, patch);

Check warning on line 254 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L254

Added line #L254 was not covered by tests

init_version_descriptor(self->py_v, &py_d);

Check warning on line 256 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L256

Added line #L256 was not covered by tests

SUCCESS;

Check warning on line 258 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L258

Added line #L258 was not covered by tests
}
}

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

Check warning on line 596 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L596

Added line #L596 was not covered by tests
}

self->is = calloc(1, self->py_v->py_is.size);
if (!isvalid(self->is)) {
log_e("Cannot allocate memory for PyInterpreterState");
goto error;

Check warning on line 602 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L601-L602

Added lines #L601 - L602 were not covered by tests
}

self->ts = calloc(1, self->py_v->py_thread.size);
if (!isvalid(self->ts)) {
log_e("Cannot allocate memory for PyThreadState");
goto error;

Check warning on line 608 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L607-L608

Added lines #L607 - L608 were not covered by tests
}

log_d("Local buffers initialised");

SUCCESS;

error:

Check warning on line 615 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L615

Added line #L615 was not covered by tests
set_error(ENOMEM);

_py_proc__free_local_buffers(self);

Check warning on line 618 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L618

Added line #L618 was not covered by tests

FAIL;

Check warning on line 620 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L620

Added line #L620 was not covered by tests
}

// ----------------------------------------------------------------------------
static int
_py_proc__find_interpreter_state(py_proc_t * self) {
Expand All @@ -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;

Check warning on line 639 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L639

Added line #L639 was not covered by tests

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 +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();

Expand Down Expand Up @@ -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.
Expand All @@ -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))) {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
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
2 changes: 1 addition & 1 deletion src/py_proc_list.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
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;

Check warning on line 954 in src/py_thread.c

View check run for this annotation

Codecov / codecov/patch

src/py_thread.c#L954

Added line #L954 was not covered by tests
}
}
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 e66cdb1

Please sign in to comment.