Skip to content

Commit 1e703a4

Browse files
carljmYhg1s
andauthored
gh-102381: don't call watcher callback with dead object (#102382)
Co-authored-by: T. Wouters <[email protected]>
1 parent a33ca2a commit 1e703a4

File tree

12 files changed

+243
-38
lines changed

12 files changed

+243
-38
lines changed

Doc/c-api/code.rst

+14-3
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,27 @@ bound into a function.
172172
before the destruction of *co* takes place, so the prior state of *co*
173173
can be inspected.
174174
175+
If *event* is ``PY_CODE_EVENT_DESTROY``, taking a reference in the callback
176+
to the about-to-be-destroyed code object will resurrect it and prevent it
177+
from being freed at this time. When the resurrected object is destroyed
178+
later, any watcher callbacks active at that time will be called again.
179+
175180
Users of this API should not rely on internal runtime implementation
176181
details. Such details may include, but are not limited to, the exact
177182
order and timing of creation and destruction of code objects. While
178183
changes in these details may result in differences observable by watchers
179184
(including whether a callback is invoked or not), it does not change
180185
the semantics of the Python code being executed.
181186
182-
If the callback returns with an exception set, it must return ``-1``; this
183-
exception will be printed as an unraisable exception using
184-
:c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
187+
If the callback sets an exception, it must return ``-1``; this exception will
188+
be printed as an unraisable exception using :c:func:`PyErr_WriteUnraisable`.
189+
Otherwise it should return ``0``.
190+
191+
There may already be a pending exception set on entry to the callback. In
192+
this case, the callback should return ``0`` with the same exception still
193+
set. This means the callback may not call any other API that can set an
194+
exception unless it saves and clears the exception state first, and restores
195+
it before returning.
185196
186197
.. versionadded:: 3.12
187198

Doc/c-api/dict.rst

+17-4
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,26 @@ Dictionary Objects
298298
dictionary.
299299
300300
The callback may inspect but must not modify *dict*; doing so could have
301-
unpredictable effects, including infinite recursion.
301+
unpredictable effects, including infinite recursion. Do not trigger Python
302+
code execution in the callback, as it could modify the dict as a side effect.
303+
304+
If *event* is ``PyDict_EVENT_DEALLOCATED``, taking a new reference in the
305+
callback to the about-to-be-destroyed dictionary will resurrect it and
306+
prevent it from being freed at this time. When the resurrected object is
307+
destroyed later, any watcher callbacks active at that time will be called
308+
again.
302309
303310
Callbacks occur before the notified modification to *dict* takes place, so
304311
the prior state of *dict* can be inspected.
305312
306-
If the callback returns with an exception set, it must return ``-1``; this
307-
exception will be printed as an unraisable exception using
308-
:c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
313+
If the callback sets an exception, it must return ``-1``; this exception will
314+
be printed as an unraisable exception using :c:func:`PyErr_WriteUnraisable`.
315+
Otherwise it should return ``0``.
316+
317+
There may already be a pending exception set on entry to the callback. In
318+
this case, the callback should return ``0`` with the same exception still
319+
set. This means the callback may not call any other API that can set an
320+
exception unless it saves and clears the exception state first, and restores
321+
it before returning.
309322
310323
.. versionadded:: 3.12

Doc/c-api/function.rst

+14-3
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,19 @@ There are a few functions specific to Python functions.
173173
runtime behavior depending on optimization decisions, it does not change
174174
the semantics of the Python code being executed.
175175
176-
If the callback returns with an exception set, it must return ``-1``; this
177-
exception will be printed as an unraisable exception using
178-
:c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
176+
If *event* is ``PyFunction_EVENT_DESTROY``, Taking a reference in the
177+
callback to the about-to-be-destroyed function will resurrect it, preventing
178+
it from being freed at this time. When the resurrected object is destroyed
179+
later, any watcher callbacks active at that time will be called again.
180+
181+
If the callback sets an exception, it must return ``-1``; this exception will
182+
be printed as an unraisable exception using :c:func:`PyErr_WriteUnraisable`.
183+
Otherwise it should return ``0``.
184+
185+
There may already be a pending exception set on entry to the callback. In
186+
this case, the callback should return ``0`` with the same exception still
187+
set. This means the callback may not call any other API that can set an
188+
exception unless it saves and clears the exception state first, and restores
189+
it before returning.
179190
180191
.. versionadded:: 3.12

Include/cpython/code.h

+9-4
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,14 @@ PyAPI_FUNC(int) PyCode_Addr2Line(PyCodeObject *, int);
224224

225225
PyAPI_FUNC(int) PyCode_Addr2Location(PyCodeObject *, int, int *, int *, int *, int *);
226226

227-
typedef enum PyCodeEvent {
228-
PY_CODE_EVENT_CREATE,
229-
PY_CODE_EVENT_DESTROY
227+
#define PY_FOREACH_CODE_EVENT(V) \
228+
V(CREATE) \
229+
V(DESTROY)
230+
231+
typedef enum {
232+
#define PY_DEF_EVENT(op) PY_CODE_EVENT_##op,
233+
PY_FOREACH_CODE_EVENT(PY_DEF_EVENT)
234+
#undef PY_DEF_EVENT
230235
} PyCodeEvent;
231236

232237

@@ -236,7 +241,7 @@ typedef enum PyCodeEvent {
236241
* The callback is invoked with a borrowed reference to co, after it is
237242
* created and before it is destroyed.
238243
*
239-
* If the callback returns with an exception set, it must return -1. Otherwise
244+
* If the callback sets an exception, it must return -1. Otherwise
240245
* it should return 0.
241246
*/
242247
typedef int (*PyCode_WatchCallback)(

Include/cpython/dictobject.h

+13-8
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ typedef struct {
1616

1717
/* Dictionary version: globally unique, value change each time
1818
the dictionary is modified */
19-
#ifdef Py_BUILD_CORE
19+
#ifdef Py_BUILD_CORE
2020
uint64_t ma_version_tag;
2121
#else
2222
Py_DEPRECATED(3.12) uint64_t ma_version_tag;
23-
#endif
23+
#endif
2424

2525
PyDictKeysObject *ma_keys;
2626

@@ -90,13 +90,18 @@ PyAPI_FUNC(PyObject *) _PyDictView_Intersect(PyObject* self, PyObject *other);
9090

9191
/* Dictionary watchers */
9292

93+
#define PY_FOREACH_DICT_EVENT(V) \
94+
V(ADDED) \
95+
V(MODIFIED) \
96+
V(DELETED) \
97+
V(CLONED) \
98+
V(CLEARED) \
99+
V(DEALLOCATED)
100+
93101
typedef enum {
94-
PyDict_EVENT_ADDED,
95-
PyDict_EVENT_MODIFIED,
96-
PyDict_EVENT_DELETED,
97-
PyDict_EVENT_CLONED,
98-
PyDict_EVENT_CLEARED,
99-
PyDict_EVENT_DEALLOCATED,
102+
#define PY_DEF_EVENT(EVENT) PyDict_EVENT_##EVENT,
103+
PY_FOREACH_DICT_EVENT(PY_DEF_EVENT)
104+
#undef PY_DEF_EVENT
100105
} PyDict_WatchEvent;
101106

102107
// Callback to be invoked when a watched dict is cleared, dealloced, or modified.

Include/cpython/funcobject.h

+8-8
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,17 @@ PyAPI_DATA(PyTypeObject) PyStaticMethod_Type;
131131
PyAPI_FUNC(PyObject *) PyClassMethod_New(PyObject *);
132132
PyAPI_FUNC(PyObject *) PyStaticMethod_New(PyObject *);
133133

134-
#define FOREACH_FUNC_EVENT(V) \
135-
V(CREATE) \
136-
V(DESTROY) \
137-
V(MODIFY_CODE) \
138-
V(MODIFY_DEFAULTS) \
134+
#define PY_FOREACH_FUNC_EVENT(V) \
135+
V(CREATE) \
136+
V(DESTROY) \
137+
V(MODIFY_CODE) \
138+
V(MODIFY_DEFAULTS) \
139139
V(MODIFY_KWDEFAULTS)
140140

141141
typedef enum {
142-
#define DEF_EVENT(EVENT) PyFunction_EVENT_##EVENT,
143-
FOREACH_FUNC_EVENT(DEF_EVENT)
144-
#undef DEF_EVENT
142+
#define PY_DEF_EVENT(EVENT) PyFunction_EVENT_##EVENT,
143+
PY_FOREACH_FUNC_EVENT(PY_DEF_EVENT)
144+
#undef PY_DEF_EVENT
145145
} PyFunction_WatchEvent;
146146

147147
/*

Include/internal/pycore_dict.h

+1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ _PyDict_NotifyEvent(PyDict_WatchEvent event,
164164
PyObject *key,
165165
PyObject *value)
166166
{
167+
assert(Py_REFCNT((PyObject*)mp) > 0);
167168
int watcher_bits = mp->ma_version_tag & DICT_VERSION_MASK;
168169
if (watcher_bits) {
169170
_PyDict_SendEvent(watcher_bits, event, mp, key, value);

Lib/test/test_capi/test_watchers.py

+50-2
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,21 @@ def test_error(self):
109109
self.watch(wid, d)
110110
with catch_unraisable_exception() as cm:
111111
d["foo"] = "bar"
112-
self.assertIs(cm.unraisable.object, d)
112+
self.assertIn(
113+
"PyDict_EVENT_ADDED watcher callback for <dict at",
114+
cm.unraisable.object
115+
)
113116
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
114117
self.assert_events([])
115118

119+
def test_dealloc_error(self):
120+
d = {}
121+
with self.watcher(kind=self.ERROR) as wid:
122+
self.watch(wid, d)
123+
with catch_unraisable_exception() as cm:
124+
del d
125+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
126+
116127
def test_two_watchers(self):
117128
d1 = {}
118129
d2 = {}
@@ -389,6 +400,25 @@ def test_code_object_events_dispatched(self):
389400
del co4
390401
self.assert_event_counts(0, 0, 0, 0)
391402

403+
def test_error(self):
404+
with self.code_watcher(2):
405+
with catch_unraisable_exception() as cm:
406+
co = _testcapi.code_newempty("test_watchers", "dummy0", 0)
407+
408+
self.assertEqual(
409+
cm.unraisable.object,
410+
f"PY_CODE_EVENT_CREATE watcher callback for {co!r}"
411+
)
412+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
413+
414+
def test_dealloc_error(self):
415+
co = _testcapi.code_newempty("test_watchers", "dummy0", 0)
416+
with self.code_watcher(2):
417+
with catch_unraisable_exception() as cm:
418+
del co
419+
420+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
421+
392422
def test_clear_out_of_range_watcher_id(self):
393423
with self.assertRaisesRegex(ValueError, r"Invalid code watcher ID -1"):
394424
_testcapi.clear_code_watcher(-1)
@@ -479,7 +509,25 @@ def watcher(*args):
479509
def myfunc():
480510
pass
481511

482-
self.assertIs(cm.unraisable.object, myfunc)
512+
self.assertEqual(
513+
cm.unraisable.object,
514+
f"PyFunction_EVENT_CREATE watcher callback for {myfunc!r}"
515+
)
516+
517+
def test_dealloc_watcher_raises_error(self):
518+
class MyError(Exception):
519+
pass
520+
521+
def watcher(*args):
522+
raise MyError("testing 123")
523+
524+
def myfunc():
525+
pass
526+
527+
with self.add_watcher(watcher):
528+
with catch_unraisable_exception() as cm:
529+
del myfunc
530+
483531
self.assertIsInstance(cm.unraisable.exc_value, MyError)
484532

485533
def test_clear_out_of_range_watcher_id(self):

Modules/_testcapi/watchers.c

+12-1
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,13 @@ noop_code_event_handler(PyCodeEvent event, PyCodeObject *co)
317317
return 0;
318318
}
319319

320+
static int
321+
error_code_event_handler(PyCodeEvent event, PyCodeObject *co)
322+
{
323+
PyErr_SetString(PyExc_RuntimeError, "boom!");
324+
return -1;
325+
}
326+
320327
static PyObject *
321328
add_code_watcher(PyObject *self, PyObject *which_watcher)
322329
{
@@ -333,7 +340,11 @@ add_code_watcher(PyObject *self, PyObject *which_watcher)
333340
num_code_object_created_events[1] = 0;
334341
num_code_object_destroyed_events[1] = 0;
335342
}
343+
else if (which_l == 2) {
344+
watcher_id = PyCode_AddWatcher(error_code_event_handler);
345+
}
336346
else {
347+
PyErr_Format(PyExc_ValueError, "invalid watcher %d", which_l);
337348
return NULL;
338349
}
339350
if (watcher_id < 0) {
@@ -672,7 +683,7 @@ _PyTestCapi_Init_Watchers(PyObject *mod)
672683
PyFunction_EVENT_##event)) { \
673684
return -1; \
674685
}
675-
FOREACH_FUNC_EVENT(ADD_EVENT);
686+
PY_FOREACH_FUNC_EVENT(ADD_EVENT);
676687
#undef ADD_EVENT
677688

678689
return 0;

Objects/codeobject.c

+37-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,24 @@
1111
#include "pycore_tuple.h" // _PyTuple_ITEMS()
1212
#include "clinic/codeobject.c.h"
1313

14+
static PyObject* code_repr(PyCodeObject *co);
15+
16+
static const char *
17+
code_event_name(PyCodeEvent event) {
18+
switch (event) {
19+
#define CASE(op) \
20+
case PY_CODE_EVENT_##op: \
21+
return "PY_CODE_EVENT_" #op;
22+
PY_FOREACH_CODE_EVENT(CASE)
23+
#undef CASE
24+
}
25+
Py_UNREACHABLE();
26+
}
27+
1428
static void
1529
notify_code_watchers(PyCodeEvent event, PyCodeObject *co)
1630
{
31+
assert(Py_REFCNT(co) > 0);
1732
PyInterpreterState *interp = _PyInterpreterState_GET();
1833
assert(interp->_initialized);
1934
uint8_t bits = interp->active_code_watchers;
@@ -25,7 +40,21 @@ notify_code_watchers(PyCodeEvent event, PyCodeObject *co)
2540
// callback must be non-null if the watcher bit is set
2641
assert(cb != NULL);
2742
if (cb(event, co) < 0) {
28-
PyErr_WriteUnraisable((PyObject *) co);
43+
// Don't risk resurrecting the object if an unraisablehook keeps
44+
// a reference; pass a string as context.
45+
PyObject *context = NULL;
46+
PyObject *repr = code_repr(co);
47+
if (repr) {
48+
context = PyUnicode_FromFormat(
49+
"%s watcher callback for %U",
50+
code_event_name(event), repr);
51+
Py_DECREF(repr);
52+
}
53+
if (context == NULL) {
54+
context = Py_NewRef(Py_None);
55+
}
56+
PyErr_WriteUnraisable(context);
57+
Py_DECREF(context);
2958
}
3059
}
3160
i++;
@@ -1667,7 +1696,14 @@ code_new_impl(PyTypeObject *type, int argcount, int posonlyargcount,
16671696
static void
16681697
code_dealloc(PyCodeObject *co)
16691698
{
1699+
assert(Py_REFCNT(co) == 0);
1700+
Py_SET_REFCNT(co, 1);
16701701
notify_code_watchers(PY_CODE_EVENT_DESTROY, co);
1702+
if (Py_REFCNT(co) > 1) {
1703+
Py_SET_REFCNT(co, Py_REFCNT(co) - 1);
1704+
return;
1705+
}
1706+
Py_SET_REFCNT(co, 0);
16711707

16721708
if (co->co_extra != NULL) {
16731709
PyInterpreterState *interp = _PyInterpreterState_GET();

0 commit comments

Comments
 (0)