* [PATCH 1/3] gdb/python: add type traits check for all PyObject sub-classes
2026-05-15 10:00 [PATCH 0/3] Use C++ type traits check to catch bugs in Python API Andrew Burgess
@ 2026-05-15 10:00 ` Andrew Burgess
2026-05-15 17:10 ` Tom Tromey
2026-05-15 10:00 ` [PATCH 2/3] gdb/python: fix use of frame_info_ptr within pending_frame_object Andrew Burgess
2026-05-15 10:00 ` [PATCH 3/3] gdb/testsuite: add a test to check for Python traits static_assert Andrew Burgess
2 siblings, 1 reply; 10+ messages in thread
From: Andrew Burgess @ 2026-05-15 10:00 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
All of our custom Python types are created as structs, like this:
struct some_new_type : public PyObject
{
... various fields ...
};
Then instances of this struct are created by calling PyObject_New,
either directly within GDB's C++ code, or within Python when a user's
Python script creates an instance of that class.
The problem is that Python is written in C, and PyObject_New doesn't
call any constructors for `some_new_type`, nor for any of the fields
within `some_new_type`.
If `some_new_type` is Plain Old Data (POD), then this is fine. Or, to
be more C++ specific, if `some_new_type` is trivially default
constructable, then we're fine.
But if a field within `some_new_type` has a non-trivial constructor,
then we're in trouble as that constructor will never be run.
An example of a problematic field type is frame_info_ptr. The
constructor for this type registers the new object with a central
management object, recording the `this` pointer, using this type within
`some_new_type` will not work as expected; frame invalidation will not
show up within the frame_info_ptr as you might expect.
And so, this type trait exists. Whenever a struct is created to define
a new Python type we should add a line like:
static_assert (gdb::is_python_allocatable_v<some_new_type>);
This will fail if any field of `some_new_type` are unsuitable for this
use.
We don't actually check is_trivially_default_constructible here. Some
types, e.g. ui_file_style::color, have non-trivial (or no default)
constructors, but are still safe to use within `some_new_type` because
their constructors just initialise data fields; there's nothing
"special" that the constructor does that cannot be achieved by
assigning the fields after creation with PyObject_New.
What actually matters is that the type is trivially destructible
(Python won't call C++ destructors, so destructors with side effects,
like deregistering from a list, would be skipped) and trivially
copyable (Python may copy objects with memcpy). Types like
frame_info_ptr, whose constructors and destructors have side effects
such as registering with a central management object, will be caught
because they are neither trivially destructible nor trivially copyable.
Simple POD types like ui_file_style are trivially destructible and
copyable, so pass this trait.
This commit adds the new type trait, and makes use of it in all cases
but one, pending_frame_object in python/py-unwind.c, has a field of
type frame_info_ptr, which is currently broken. This will be fixed,
and the static_assert added, in the next commit.
---
gdb/python/py-arch.c | 2 +
gdb/python/py-block.c | 4 ++
gdb/python/py-breakpoint.c | 2 +
gdb/python/py-cmd.c | 2 +
gdb/python/py-color.c | 2 +
gdb/python/py-connection.c | 2 +
gdb/python/py-corefile.c | 4 ++
gdb/python/py-disasm.c | 8 +++
gdb/python/py-events.h | 2 +
gdb/python/py-frame.c | 2 +
gdb/python/py-instruction.c | 2 +
gdb/python/py-lazy-string.c | 2 +
gdb/python/py-linetable.c | 6 +++
gdb/python/py-membuf.c | 2 +
gdb/python/py-micmd.c | 2 +
gdb/python/py-param.c | 2 +
gdb/python/py-prettyprint.c | 2 +
gdb/python/py-record-btrace.c | 2 +
gdb/python/py-record.c | 2 +
gdb/python/py-record.h | 4 ++
gdb/python/py-ref.h | 3 ++
gdb/python/py-registers.c | 8 +++
gdb/python/py-style.c | 2 +
gdb/python/py-symbol.c | 2 +
gdb/python/py-symtab.c | 4 ++
gdb/python/py-tui.c | 2 +
gdb/python/py-type.c | 4 ++
gdb/python/py-unwind.c | 2 +
gdb/python/py-value.c | 2 +
gdb/python/python-internal.h | 2 +
gdb/python/python-traits.h | 92 +++++++++++++++++++++++++++++++++++
31 files changed, 179 insertions(+)
create mode 100644 gdb/python/python-traits.h
diff --git a/gdb/python/py-arch.c b/gdb/python/py-arch.c
index ac0c40dcf45..7a0cb0a2a59 100644
--- a/gdb/python/py-arch.c
+++ b/gdb/python/py-arch.c
@@ -27,6 +27,8 @@ struct arch_object : public PyObject
struct gdbarch *gdbarch;
};
+static_assert (gdb::is_python_allocatable_v<arch_object>);
+
static const registry<gdbarch>::key<PyObject, gdb::noop_deleter<PyObject>>
arch_object_data;
diff --git a/gdb/python/py-block.c b/gdb/python/py-block.c
index d9751d26378..39ec52ae373 100644
--- a/gdb/python/py-block.c
+++ b/gdb/python/py-block.c
@@ -33,6 +33,8 @@ struct block_object : public PyObject
struct objfile *objfile;
};
+static_assert (gdb::is_python_allocatable_v<block_object>);
+
struct block_syms_iterator_object : public PyObject
{
/* The block. */
@@ -47,6 +49,8 @@ struct block_syms_iterator_object : public PyObject
block_object *source;
};
+static_assert (gdb::is_python_allocatable_v<block_syms_iterator_object>);
+
/* Require a valid block. All access to block_object->block should be
gated by this call. */
#define BLPY_REQUIRE_VALID(block_obj, block) \
diff --git a/gdb/python/py-breakpoint.c b/gdb/python/py-breakpoint.c
index d96c67694b3..fb6fab6b60b 100644
--- a/gdb/python/py-breakpoint.c
+++ b/gdb/python/py-breakpoint.c
@@ -46,6 +46,8 @@ struct gdbpy_breakpoint_location_object : public PyObject
gdbpy_breakpoint_object *owner;
};
+static_assert (gdb::is_python_allocatable_v<gdbpy_breakpoint_location_object>);
+
/* Require that BREAKPOINT and LOCATION->OWNER are the same; throw a Python
exception if they are not. */
#define BPLOCPY_REQUIRE_VALID(Breakpoint, Location) \
diff --git a/gdb/python/py-cmd.c b/gdb/python/py-cmd.c
index 3fbc26b0cde..a1a02bd7eb5 100644
--- a/gdb/python/py-cmd.c
+++ b/gdb/python/py-cmd.c
@@ -63,6 +63,8 @@ struct cmdpy_object : public PyObject
struct cmd_list_element *sub_list;
};
+static_assert (gdb::is_python_allocatable_v<cmdpy_object>);
+
extern PyTypeObject cmdpy_object_type;
/* Constants used by this module. */
diff --git a/gdb/python/py-color.c b/gdb/python/py-color.c
index 971209958cf..29a42977adc 100644
--- a/gdb/python/py-color.c
+++ b/gdb/python/py-color.c
@@ -43,6 +43,8 @@ struct colorpy_object : public PyObject
ui_file_style::color color;
};
+static_assert (gdb::is_python_allocatable_v<colorpy_object>);
+
extern PyTypeObject colorpy_object_type;
/* See py-color.h. */
diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c
index a8bea4d832f..8f9fc4a7eee 100644
--- a/gdb/python/py-connection.c
+++ b/gdb/python/py-connection.c
@@ -42,6 +42,8 @@ struct connection_object : public PyObject
struct process_stratum_target *target;
};
+static_assert (gdb::is_python_allocatable_v<connection_object>);
+
extern PyTypeObject connection_object_type;
extern PyTypeObject remote_connection_object_type;
diff --git a/gdb/python/py-corefile.c b/gdb/python/py-corefile.c
index 0fa4e90488c..1cdbae98149 100644
--- a/gdb/python/py-corefile.c
+++ b/gdb/python/py-corefile.c
@@ -65,6 +65,8 @@ struct corefile_mapped_file_object : public PyObject
bool is_main_exec_p;
};
+static_assert (gdb::is_python_allocatable_v<corefile_mapped_file_object>);
+
extern PyTypeObject corefile_mapped_file_object_type;
/* A gdb.CorefileMappedFileRegion object. */
@@ -80,6 +82,8 @@ struct corefile_mapped_file_region_object : public PyObject
ULONGEST file_offset;
};
+static_assert (gdb::is_python_allocatable_v<corefile_mapped_file_region_object>);
+
extern PyTypeObject corefile_mapped_file_region_object_type;
/* Clear the inferior pointer in a Corefile object OBJ when an inferior is
diff --git a/gdb/python/py-disasm.c b/gdb/python/py-disasm.c
index 1c5661dd307..9591cb82133 100644
--- a/gdb/python/py-disasm.c
+++ b/gdb/python/py-disasm.c
@@ -50,6 +50,8 @@ struct disasm_info_object : public PyObject
struct disasm_info_object *next;
};
+static_assert (gdb::is_python_allocatable_v<disasm_info_object>);
+
extern PyTypeObject disasm_info_object_type;
/* Implement gdb.disassembler.DisassembleAddressPart type. An object of
@@ -69,6 +71,8 @@ struct disasm_addr_part_object : public PyObject
struct gdbarch *gdbarch;
};
+static_assert (gdb::is_python_allocatable_v<disasm_addr_part_object>);
+
extern PyTypeObject disasm_addr_part_object_type;
/* Implement gdb.disassembler.DisassembleTextPart type. An object of
@@ -84,6 +88,8 @@ struct disasm_text_part_object : public PyObject
enum disassembler_style style;
};
+static_assert (gdb::is_python_allocatable_v<disasm_text_part_object>);
+
extern PyTypeObject disasm_text_part_object_type;
extern PyTypeObject disasm_part_object_type;
@@ -103,6 +109,8 @@ struct disasm_result_object : public PyObject
std::vector<gdbpy_ref<>> *parts;
};
+static_assert (gdb::is_python_allocatable_v<disasm_result_object>);
+
extern PyTypeObject disasm_result_object_type;
/* When this is false we fast path out of gdbpy_print_insn, which should
diff --git a/gdb/python/py-events.h b/gdb/python/py-events.h
index e44b4b4a761..169a5f4a4db 100644
--- a/gdb/python/py-events.h
+++ b/gdb/python/py-events.h
@@ -32,6 +32,8 @@ struct eventregistry_object : public PyObject
PyObject *callbacks;
};
+static_assert (gdb::is_python_allocatable_v<eventregistry_object>);
+
/* Struct holding references to event registries both in python and c.
This is meant to be a singleton. */
diff --git a/gdb/python/py-frame.c b/gdb/python/py-frame.c
index 1420d2ac5b9..ad590c7f55a 100644
--- a/gdb/python/py-frame.c
+++ b/gdb/python/py-frame.c
@@ -44,6 +44,8 @@ struct frame_object : public PyObject
int frame_id_is_next;
};
+static_assert (gdb::is_python_allocatable_v<frame_object>);
+
/* Require a valid frame. This must be called inside a TRY_CATCH, or
another context in which a gdb exception is allowed. */
#define FRAPY_REQUIRE_VALID(frame_obj, frame) \
diff --git a/gdb/python/py-instruction.c b/gdb/python/py-instruction.c
index df64d85988f..d8dad094ee9 100644
--- a/gdb/python/py-instruction.c
+++ b/gdb/python/py-instruction.c
@@ -32,6 +32,8 @@ PyTypeObject py_insn_type = {
struct py_insn_obj: public PyObject
{};
+static_assert (gdb::is_python_allocatable_v<py_insn_obj>);
+
/* Getter function for gdb.Instruction attributes. */
static PyObject *
diff --git a/gdb/python/py-lazy-string.c b/gdb/python/py-lazy-string.c
index fe191451e54..79592b10aa6 100644
--- a/gdb/python/py-lazy-string.c
+++ b/gdb/python/py-lazy-string.c
@@ -51,6 +51,8 @@ struct lazy_string_object : public PyObject
PyObject *type;
};
+static_assert (gdb::is_python_allocatable_v<lazy_string_object>);
+
extern PyTypeObject lazy_string_object_type;
static PyObject *
diff --git a/gdb/python/py-linetable.c b/gdb/python/py-linetable.c
index 2afa9b033e7..c0368355148 100644
--- a/gdb/python/py-linetable.c
+++ b/gdb/python/py-linetable.c
@@ -27,6 +27,8 @@ struct linetable_entry_object : public PyObject
CORE_ADDR pc;
};
+static_assert (gdb::is_python_allocatable_v<linetable_entry_object>);
+
extern PyTypeObject linetable_entry_object_type;
struct linetable_object : public PyObject
@@ -37,6 +39,8 @@ struct linetable_object : public PyObject
PyObject *symtab;
};
+static_assert (gdb::is_python_allocatable_v<linetable_object>);
+
extern PyTypeObject linetable_object_type;
struct ltpy_iterator_object : public PyObject
@@ -49,6 +53,8 @@ struct ltpy_iterator_object : public PyObject
PyObject *source;
};
+static_assert (gdb::is_python_allocatable_v<ltpy_iterator_object>);
+
extern PyTypeObject ltpy_iterator_object_type;
/* Internal helper function to extract gdb.Symtab from a gdb.LineTable
diff --git a/gdb/python/py-membuf.c b/gdb/python/py-membuf.c
index e3bf5e2ceab..fa5156b885e 100644
--- a/gdb/python/py-membuf.c
+++ b/gdb/python/py-membuf.c
@@ -31,6 +31,8 @@ struct membuf_object : public PyObject
CORE_ADDR length;
};
+static_assert (gdb::is_python_allocatable_v<membuf_object>);
+
extern PyTypeObject membuf_object_type;
/* Wrap BUFFER, ADDRESS, and LENGTH into a gdb.Membuf object. ADDRESS is
diff --git a/gdb/python/py-micmd.c b/gdb/python/py-micmd.c
index 6edea04ccb5..241e80bea5b 100644
--- a/gdb/python/py-micmd.c
+++ b/gdb/python/py-micmd.c
@@ -74,6 +74,8 @@ struct micmdpy_object : public PyObject
char *mi_command_name;
};
+static_assert (gdb::is_python_allocatable_v<micmdpy_object>);
+
/* The MI command implemented in Python. */
struct mi_command_py : public mi_command
diff --git a/gdb/python/py-param.c b/gdb/python/py-param.c
index bc028f1746c..35f55c3dbd0 100644
--- a/gdb/python/py-param.c
+++ b/gdb/python/py-param.c
@@ -141,6 +141,8 @@ struct parmpy_object : public PyObject
const char **enumeration;
};
+static_assert (gdb::is_python_allocatable_v<parmpy_object>);
+
/* Wraps a setting around an existing parmpy_object. This abstraction
is used to manipulate the value in S->VALUE in a type safe manner using
the setting interface. */
diff --git a/gdb/python/py-prettyprint.c b/gdb/python/py-prettyprint.c
index 0cf0cde881c..b0a296d8010 100644
--- a/gdb/python/py-prettyprint.c
+++ b/gdb/python/py-prettyprint.c
@@ -784,6 +784,8 @@ gdbpy_get_print_options (value_print_options *opts)
struct printer_object : public PyObject
{};
+static_assert (gdb::is_python_allocatable_v<printer_object>);
+
/* The ValuePrinter type object. */
PyTypeObject printer_object_type =
{
diff --git a/gdb/python/py-record-btrace.c b/gdb/python/py-record-btrace.c
index 6026de91e67..267429832d9 100644
--- a/gdb/python/py-record-btrace.c
+++ b/gdb/python/py-record-btrace.c
@@ -48,6 +48,8 @@ struct btpy_list_object : public PyObject
PyTypeObject* element_type;
};
+static_assert (gdb::is_python_allocatable_v<btpy_list_object>);
+
/* Python type for btrace lists. */
static PyTypeObject btpy_list_type = {
diff --git a/gdb/python/py-record.c b/gdb/python/py-record.c
index 294da0725e1..5394d63c803 100644
--- a/gdb/python/py-record.c
+++ b/gdb/python/py-record.c
@@ -67,6 +67,8 @@ struct recpy_gap_object : public PyObject
Py_ssize_t number;
};
+static_assert (gdb::is_python_allocatable_v<recpy_gap_object>);
+
/* Implementation of record.method. */
static PyObject *
diff --git a/gdb/python/py-record.h b/gdb/python/py-record.h
index 35ab54cf6ab..f62fcc2a7e9 100644
--- a/gdb/python/py-record.h
+++ b/gdb/python/py-record.h
@@ -34,6 +34,8 @@ struct recpy_record_object : public PyObject
enum record_method method;
};
+static_assert (gdb::is_python_allocatable_v<recpy_record_object>);
+
/* Python recorded element object. This is generic enough to represent
recorded instructions as well as recorded function call segments, hence the
generic name. */
@@ -49,6 +51,8 @@ struct recpy_element_object : public PyObject
Py_ssize_t number;
};
+static_assert (gdb::is_python_allocatable_v<recpy_element_object>);
+
/* Python RecordInstruction type. */
extern PyTypeObject recpy_insn_type;
diff --git a/gdb/python/py-ref.h b/gdb/python/py-ref.h
index dc0b14814af..3d2906fd7f5 100644
--- a/gdb/python/py-ref.h
+++ b/gdb/python/py-ref.h
@@ -21,6 +21,7 @@
#define GDB_PYTHON_PY_REF_H
#include "gdbsupport/gdb_ref_ptr.h"
+#include "python-traits.h"
/* A policy class for gdb::ref_ptr for Python reference counting. */
struct gdbpy_ref_policy
@@ -102,4 +103,6 @@ struct gdbpy_dict_wrapper : public PyObject
}
};
+static_assert (gdb::is_python_allocatable_v<gdbpy_dict_wrapper>);
+
#endif /* GDB_PYTHON_PY_REF_H */
diff --git a/gdb/python/py-registers.c b/gdb/python/py-registers.c
index c6e0748e935..af75cb81bbe 100644
--- a/gdb/python/py-registers.c
+++ b/gdb/python/py-registers.c
@@ -45,6 +45,8 @@ struct register_descriptor_iterator_object : public PyObject
struct gdbarch *gdbarch;
};
+static_assert (gdb::is_python_allocatable_v<register_descriptor_iterator_object>);
+
extern PyTypeObject register_descriptor_iterator_object_type;
/* A register descriptor. */
@@ -57,6 +59,8 @@ struct register_descriptor_object : public PyObject
struct gdbarch *gdbarch;
};
+static_assert (gdb::is_python_allocatable_v<register_descriptor_object>);
+
extern PyTypeObject register_descriptor_object_type;
/* Structure for iterator over register groups. */
@@ -69,6 +73,8 @@ struct reggroup_iterator_object : public PyObject
struct gdbarch *gdbarch;
};
+static_assert (gdb::is_python_allocatable_v<reggroup_iterator_object>);
+
extern PyTypeObject reggroup_iterator_object_type;
/* A register group object. */
@@ -78,6 +84,8 @@ struct reggroup_object : public PyObject
const struct reggroup *reggroup;
};
+static_assert (gdb::is_python_allocatable_v<reggroup_object>);
+
extern PyTypeObject reggroup_object_type;
/* Map from GDB's internal reggroup objects to the Python
diff --git a/gdb/python/py-style.c b/gdb/python/py-style.c
index 60a9ea4792f..c45eba60867 100644
--- a/gdb/python/py-style.c
+++ b/gdb/python/py-style.c
@@ -46,6 +46,8 @@ struct style_object : public PyObject
char *style_name;
};
+static_assert (gdb::is_python_allocatable_v<style_object>);
+
extern PyTypeObject style_object_type;
/* Initialize the 'style' module. */
diff --git a/gdb/python/py-symbol.c b/gdb/python/py-symbol.c
index 6293b658ea1..a3b10d3df24 100644
--- a/gdb/python/py-symbol.c
+++ b/gdb/python/py-symbol.c
@@ -31,6 +31,8 @@ struct symbol_object : public PyObject
struct symbol *symbol;
};
+static_assert (gdb::is_python_allocatable_v<symbol_object>);
+
/* Require a valid symbol. All access to symbol_object->symbol should be
gated by this call. */
#define SYMPY_REQUIRE_VALID(symbol_obj, symbol) \
diff --git a/gdb/python/py-symtab.c b/gdb/python/py-symtab.c
index c5e18543740..dffddecb412 100644
--- a/gdb/python/py-symtab.c
+++ b/gdb/python/py-symtab.c
@@ -32,6 +32,8 @@ struct symtab_object : public PyObject
struct symtab *symtab;
};
+static_assert (gdb::is_python_allocatable_v<symtab_object>);
+
extern PyTypeObject symtab_object_type;
static const gdbpy_registry<gdbpy_memoizing_registry_storage<symtab_object,
symtab, &symtab_object::symtab>> stpy_registry;
@@ -61,6 +63,8 @@ struct sal_object : public PyObject
sal_object *next;
};
+static_assert (gdb::is_python_allocatable_v<sal_object>);
+
/* This is called when an objfile is about to be freed. Invalidate
the sal object as further actions on the sal would result in bad
data. All access to obj->sal should be gated by
diff --git a/gdb/python/py-tui.c b/gdb/python/py-tui.c
index be19193770f..4db04941d2d 100644
--- a/gdb/python/py-tui.c
+++ b/gdb/python/py-tui.c
@@ -53,6 +53,8 @@ struct gdbpy_tui_window: public PyObject
bool is_valid () const;
};
+static_assert (gdb::is_python_allocatable_v<gdbpy_tui_window>);
+
extern PyTypeObject gdbpy_tui_window_object_type;
/* A TUI window written in Python. */
diff --git a/gdb/python/py-type.c b/gdb/python/py-type.c
index 263d48a9365..4b6ac0e9eef 100644
--- a/gdb/python/py-type.c
+++ b/gdb/python/py-type.c
@@ -33,6 +33,8 @@ struct type_object : public PyObject
struct type *type;
};
+static_assert (gdb::is_python_allocatable_v<type_object>);
+
extern PyTypeObject type_object_type;
/* A Field object. */
@@ -52,6 +54,8 @@ struct typy_iterator_object : public PyObject
type_object *source;
};
+static_assert (gdb::is_python_allocatable_v<typy_iterator_object>);
+
extern PyTypeObject type_iterator_object_type;
/* This is used to initialize various gdb.TYPE_ constants. */
diff --git a/gdb/python/py-unwind.c b/gdb/python/py-unwind.c
index dcf86f7db3d..ed0ba89d267 100644
--- a/gdb/python/py-unwind.c
+++ b/gdb/python/py-unwind.c
@@ -104,6 +104,8 @@ struct unwind_info_object : public PyObject
std::vector<saved_reg> *saved_regs;
};
+static_assert (gdb::is_python_allocatable_v<unwind_info_object>);
+
/* The data we keep for a frame we can unwind: frame ID and an array of
(register_number, register_value) pairs. */
diff --git a/gdb/python/py-value.c b/gdb/python/py-value.c
index a5b2303728c..ca88a35e357 100644
--- a/gdb/python/py-value.c
+++ b/gdb/python/py-value.c
@@ -64,6 +64,8 @@ struct value_object : public PyObject
PyObject *content_bytes;
};
+static_assert (gdb::is_python_allocatable_v<value_object>);
+
/* List of all values which are currently exposed to Python. It is
maintained so that when an objfile is discarded, preserve_values
can copy the values' types if needed. */
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 37bc37691fe..ac01fa5a385 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -364,6 +364,8 @@ struct gdbpy_breakpoint_object : public PyObject
int is_finish_bp;
};
+static_assert (gdb::is_python_allocatable_v<gdbpy_breakpoint_object>);
+
/* Require that BREAKPOINT be a valid breakpoint ID; throw a Python
exception if it is invalid. */
#define BPPY_REQUIRE_VALID(Breakpoint) \
diff --git a/gdb/python/python-traits.h b/gdb/python/python-traits.h
new file mode 100644
index 00000000000..7babb0fc131
--- /dev/null
+++ b/gdb/python/python-traits.h
@@ -0,0 +1,92 @@
+/* Type traits relating to GDB's Python integration.
+
+ Copyright (C) 2008-2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDB_PYTHON_PYTHON_TRAITS_H
+#define GDB_PYTHON_PYTHON_TRAITS_H
+
+namespace gdb
+{
+/* All of our custom Python types are created as structs, like this:
+
+ struct some_new_type : public PyObject
+ {
+ ... various fields ...
+ };
+
+ Then instances of this struct are created by calling PyObject_New,
+ either directly within GDB's C++ code, or within Python when a user's
+ Python script creates an instance of that class.
+
+ The problem is that Python is written in C, and PyObject_New doesn't
+ call any constructors for `some_new_type`, nor for any of the fields
+ within `some_new_type`.
+
+ If `some_new_type` is Plain Old Data (POD), then this is fine. Or, to
+ be more C++ specific, if `some_new_type` is trivially default
+ constructable, then we're fine.
+
+ But if a field within `some_new_type` has a non-trivial constructor,
+ then we're in trouble as that constructor will never be run.
+
+ An example of a problematic field type is frame_info_ptr. The
+ constructor for this type registers the new object with a central
+ management object, recording the `this` pointer, using this type within
+ `some_new_type` will not work as expected; frame invalidation will not
+ show up within the frame_info_ptr as you might expect.
+
+ And so, this type trait exists. Whenever a struct is created to define
+ a new Python type we should add a line like:
+
+ static_assert (gdb::is_python_allocatable_v<some_new_type>);
+
+ This will fail if any field of `some_new_type` is unsuitable for this
+ use.
+
+ We don't actually check is_trivially_default_constructible here. Some
+ types, e.g. ui_file_style::color, have non-trivial (or no default)
+ constructors, but are still safe to use within `some_new_type` because
+ their constructors just initialise data fields; there's nothing
+ "special" that the constructor does that cannot be achieved by
+ assigning the fields after creation with PyObject_New.
+
+ What actually matters is that the type is trivially destructible
+ (Python won't call C++ destructors, so destructors with side effects,
+ like deregistering from a list, would be skipped) and trivially
+ copyable (Python may copy objects with memcpy). Types like
+ frame_info_ptr, whose constructors and destructors have side effects
+ such as registering with a central management object, will be caught
+ because they are neither trivially destructible nor trivially copyable.
+ Types like ui_file_style are trivially destructible and copyable, so
+ pass this trait. */
+
+template <typename T>
+struct is_python_allocatable {
+ static constexpr bool value =
+ std::is_trivially_destructible_v<T> &&
+ std::is_trivially_copyable_v<T>;
+};
+
+/* Helper for the above trait to make it more usable. */
+
+template <typename T>
+inline constexpr bool is_python_allocatable_v
+ = is_python_allocatable<T>::value;
+}
+
+#endif /* GDB_PYTHON_PYTHON_TRAITS_H */
--
2.25.4
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH 2/3] gdb/python: fix use of frame_info_ptr within pending_frame_object
2026-05-15 10:00 [PATCH 0/3] Use C++ type traits check to catch bugs in Python API Andrew Burgess
2026-05-15 10:00 ` [PATCH 1/3] gdb/python: add type traits check for all PyObject sub-classes Andrew Burgess
@ 2026-05-15 10:00 ` Andrew Burgess
2026-05-15 17:22 ` Tom Tromey
2026-05-15 10:00 ` [PATCH 3/3] gdb/testsuite: add a test to check for Python traits static_assert Andrew Burgess
2 siblings, 1 reply; 10+ messages in thread
From: Andrew Burgess @ 2026-05-15 10:00 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
The previous commit added a type trait which identifies types that
should not be used within Python objects, that is, types that are not
trivially default constructible. As a result of this, it was
discovered that pending_frame_object includes a frame_info_ptr field.
The problem with frame_info_ptr is that its constructor registers the
new frame_info_ptr with the global frame_list. It is by this
registration that invalidation of frame_info_ptr objects is performed.
As Python is written in C, C++ constructors are not called, so when a
pending_frame_object is created the constructor for the nested
frame_info_ptr field is never run, and the frame_info_ptr is never
registered with the global frame_list. As a result the frame_info_ptr
will never be invalidated if the frame cache is flushed, this can then
lead to problems where we make use of the 'frame_info *' within the
frame_info_ptr, even though it is no longer valid.
In this commit I change the frame_info_ptr within pending_frame_object
to a 'frame_info_ptr *' and allocate the frame_info_ptr object on the
heap, releasing the object, and resetting the point to NULL, when we
are done with it. As the pending_frame_object only needs to remain
valid for the duration of frame_unwind_python::sniff, the 'new' and
'delete' both performed within the function.
We can now check that a pending_frame_object is valid by checking if
the 'frame_info_ptr *' is NULL or not. As the frame_info_ptr is
created in a valid state, and the point is set back to NULL when we
are done with it, we no longer need to compare the frame_info_ptr
object itself against NULL.
The remaining changes in this patch are to dereference the
'frame_info_ptr *' in places where we need the actual object. In some
cases I need to move the dereference later within a function, after a
validity check, in order to avoid dereferencing a NULL pointer.
Finally, I can add the static_assert that guarantees that
pending_frame_object is now safe for allocation by Python.
I discovered this bug while looking at PR gdb/32120. That bug is
about a user's custom frame unwinder that triggers a flush of the
frame cache during the sniffer phase (the
RemoteTargetConnection.send_packet call switches thread, which
triggers the frame cache flush). While looking at that bug I noticed
that the frame_info_ptr within the pending_frame_object wasn't being
reset when the frame cache was flushed. Fixing this does not resolve
the user's issue, but I thought it was still worth tagging this commit
with the bug link.
Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32120
---
gdb/python/py-unwind.c | 53 ++++++++++++++++++++++++++----------------
1 file changed, 33 insertions(+), 20 deletions(-)
diff --git a/gdb/python/py-unwind.c b/gdb/python/py-unwind.c
index ed0ba89d267..fd305cb3316 100644
--- a/gdb/python/py-unwind.c
+++ b/gdb/python/py-unwind.c
@@ -68,13 +68,17 @@ show_pyuw_debug (struct ui_file *file, int from_tty,
struct pending_frame_object : public PyObject
{
- /* Frame we are unwinding. */
- frame_info_ptr frame_info;
+ /* Frame we are unwinding. We cannot place a frame_info_ptr
+ directly within this struct as it is not trivially default
+ constructable. */
+ frame_info_ptr *frame_info;
/* Its architecture, passed by the sniffer caller. */
struct gdbarch *gdbarch;
};
+static_assert (gdb::is_python_allocatable_v<pending_frame_object>);
+
/* Saved registers array item. */
struct saved_reg
@@ -245,9 +249,8 @@ unwind_infopy_repr (PyObject *self)
unwind_info_object *unwind_info = (unwind_info_object *) self;
pending_frame_object *pending_frame
= (pending_frame_object *) (unwind_info->pending_frame);
- frame_info_ptr frame = pending_frame->frame_info;
- if (frame == nullptr)
+ if (pending_frame->frame_info == nullptr)
return PyUnicode_FromFormat ("<%s for an invalid frame>",
gdbpy_py_obj_tp_name (self));
@@ -263,6 +266,7 @@ unwind_infopy_repr (PyObject *self)
saved_reg_names = (saved_reg_names + ", ") + name;
}
+ frame_info_ptr frame (*pending_frame->frame_info);
return PyUnicode_FromFormat ("<%s frame #%d, saved_regs=(%s)>",
gdbpy_py_obj_tp_name (self),
frame_relative_level (frame),
@@ -331,7 +335,7 @@ unwind_infopy_add_saved_register (PyObject *self, PyObject *args, PyObject *kw)
if (regnum >= gdbarch_num_cooked_regs (pending_frame->gdbarch))
{
struct value *user_reg_value
- = value_of_user_reg (regnum, pending_frame->frame_info);
+ = value_of_user_reg (regnum, *pending_frame->frame_info);
if (user_reg_value->lval () == lval_register)
regnum = user_reg_value->regnum ();
if (regnum >= gdbarch_num_cooked_regs (pending_frame->gdbarch))
@@ -414,14 +418,15 @@ unwind_infopy_dealloc (PyObject *self)
static PyObject *
pending_framepy_str (PyObject *self)
{
- frame_info_ptr frame = ((pending_frame_object *) self)->frame_info;
+ pending_frame_object *pending_frame = (pending_frame_object *) self;
const char *sp_str = NULL;
const char *pc_str = NULL;
- if (frame == NULL)
+ if (pending_frame->frame_info == nullptr)
return PyUnicode_FromString ("Stale PendingFrame instance");
try
{
+ frame_info_ptr frame (*pending_frame->frame_info);
sp_str = core_addr_to_string_nz (get_frame_sp (frame));
pc_str = core_addr_to_string_nz (get_frame_pc (frame));
}
@@ -439,14 +444,15 @@ static PyObject *
pending_framepy_repr (PyObject *self)
{
pending_frame_object *pending_frame = (pending_frame_object *) self;
- frame_info_ptr frame = pending_frame->frame_info;
- if (frame == nullptr)
+ if (pending_frame->frame_info == nullptr)
return gdb_py_invalid_object_repr (self);
const char *sp_str = nullptr;
const char *pc_str = nullptr;
+ frame_info_ptr frame (*pending_frame->frame_info);
+
try
{
sp_str = core_addr_to_string_nz (get_frame_sp (frame));
@@ -493,7 +499,7 @@ pending_framepy_read_register (PyObject *self, PyObject *args, PyObject *kw)
get_frame_register_value() was used here, which did not
handle the user register case. */
value *val = value_of_register
- (regnum, get_next_frame_sentinel_okay (pending_frame->frame_info));
+ (regnum, get_next_frame_sentinel_okay (*pending_frame->frame_info));
if (val == NULL)
PyErr_Format (PyExc_ValueError,
"Cannot read register %d from frame.",
@@ -520,6 +526,10 @@ pending_framepy_is_valid (PyObject *self, PyObject *args)
if (pending_frame->frame_info == nullptr)
Py_RETURN_FALSE;
+ /* The frame_info field should never point at an uninitialized
+ object. */
+ gdb_assert (*pending_frame->frame_info != nullptr);
+
Py_RETURN_TRUE;
}
@@ -538,7 +548,7 @@ pending_framepy_name (PyObject *self, PyObject *args)
try
{
enum language lang;
- frame_info_ptr frame = pending_frame->frame_info;
+ frame_info_ptr frame = *pending_frame->frame_info;
name = find_frame_funname (frame, &lang, nullptr);
}
@@ -568,7 +578,7 @@ pending_framepy_pc (PyObject *self, PyObject *args)
try
{
- pc = get_frame_pc (pending_frame->frame_info);
+ pc = get_frame_pc (*pending_frame->frame_info);
}
catch (const gdb_exception &except)
{
@@ -590,7 +600,7 @@ pending_framepy_language (PyObject *self, PyObject *args)
try
{
- frame_info_ptr fi = pending_frame->frame_info;
+ frame_info_ptr fi = *pending_frame->frame_info;
enum language lang = get_frame_language (fi);
const language_defn *lang_def = language_def (lang);
@@ -615,7 +625,7 @@ pending_framepy_find_sal (PyObject *self, PyObject *args)
try
{
- frame_info_ptr frame = pending_frame->frame_info;
+ frame_info_ptr frame = *pending_frame->frame_info;
symtab_and_line sal = find_frame_sal (frame);
return symtab_and_line_to_sal_object (sal).release ();
@@ -636,7 +646,7 @@ pending_framepy_block (PyObject *self, PyObject *args)
PENDING_FRAMEPY_REQUIRE_VALID (pending_frame);
- frame_info_ptr frame = pending_frame->frame_info;
+ frame_info_ptr frame = *pending_frame->frame_info;
const struct block *block = nullptr, *fn_block;
try
@@ -682,7 +692,7 @@ pending_framepy_function (PyObject *self, PyObject *args)
try
{
enum language funlang;
- frame_info_ptr frame = pending_frame->frame_info;
+ frame_info_ptr frame = *pending_frame->frame_info;
gdb::unique_xmalloc_ptr<char> funname
= find_frame_funname (frame, &funlang, &sym);
@@ -774,7 +784,7 @@ pending_framepy_level (PyObject *self, PyObject *args)
PENDING_FRAMEPY_REQUIRE_VALID (pending_frame);
- int level = frame_relative_level (pending_frame->frame_info);
+ int level = frame_relative_level (*pending_frame->frame_info);
return gdb_py_object_from_longest (level).release ();
}
@@ -863,9 +873,12 @@ frame_unwind_python::sniff (const frame_info_ptr &this_frame,
return 0;
}
pfo->gdbarch = gdbarch;
- pfo->frame_info = nullptr;
- scoped_restore invalidate_frame = make_scoped_restore (&pfo->frame_info,
- this_frame);
+ pfo->frame_info = new frame_info_ptr (this_frame);
+ SCOPE_EXIT
+ {
+ delete pfo->frame_info;
+ pfo->frame_info = nullptr;
+ };
/* Run unwinders. */
if (gdb_python_module == NULL
--
2.25.4
^ permalink raw reply [flat|nested] 10+ messages in thread