Skip to content

Commit adb460a

Browse files
committed
[GR-50212] Avoid shutdown dealloc of native-owned weakrefs
PullRequest: graalpython/4524
2 parents fc57b60 + 67d5b3e commit adb460a

2 files changed

Lines changed: 42 additions & 14 deletions

File tree

  • graalpython

graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_shutdown.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def test_sigterm():
7979

8080
@skip_if_sandboxed("Needs native extension support in sandboxed runs")
8181
@skipIf(not GRAALPY, "GraalPy-only native weakref shutdown test")
82-
def test_native_weakref_shutdown_with_c_retained_object():
82+
def test_native_weakref_shutdown_skips_c_retained_object():
8383
module = compile_module_from_string(textwrap.dedent("""
8484
#include "Python.h"
8585
#include "structmember.h"
@@ -90,23 +90,34 @@ def test_native_weakref_shutdown_with_c_retained_object():
9090
typedef struct {
9191
PyObject_HEAD
9292
PyObject *weakreflist;
93+
int id;
9394
} NativeWeakRefObject;
9495
9596
static PyObject *kept_alive;
97+
static PyTypeObject NativeWeakRefType;
9698
9799
static void write_marker(const char *contents) {
98100
const char *marker_path = getenv("GR50212_DEALLOC_MARKER");
99101
if (marker_path != NULL) {
100-
FILE *marker = fopen(marker_path, "w");
102+
FILE *marker = fopen(marker_path, "a");
101103
if (marker != NULL) {
102104
fputs(contents, marker);
103105
fclose(marker);
104106
}
105107
}
106108
}
107109
110+
static PyObject *new_native_weakref(int id) {
111+
NativeWeakRefObject *obj = (NativeWeakRefObject *)PyObject_CallNoArgs((PyObject *)&NativeWeakRefType);
112+
if (obj == NULL) {
113+
return NULL;
114+
}
115+
obj->id = id;
116+
return (PyObject *)obj;
117+
}
118+
108119
static void NativeWeakRef_dealloc(NativeWeakRefObject *self) {
109-
write_marker("deallocated\\n");
120+
write_marker(self->id == 1 ? "deallocated:held\\n" : "deallocated:free\\n");
110121
if (self->weakreflist != NULL) {
111122
PyObject_ClearWeakRefs((PyObject *)self);
112123
}
@@ -125,16 +136,26 @@ def test_native_weakref_shutdown_with_c_retained_object():
125136
126137
static PyObject *hold(PyObject *self, PyObject *Py_UNUSED(ignored)) {
127138
Py_CLEAR(kept_alive);
128-
kept_alive = PyObject_CallNoArgs((PyObject *)&NativeWeakRefType);
139+
kept_alive = new_native_weakref(1);
129140
if (kept_alive == NULL) {
130141
return NULL;
131142
}
132-
write_marker("held\\n");
143+
write_marker("created:held\\n");
133144
return Py_NewRef(kept_alive);
134145
}
135146
147+
static PyObject *make(PyObject *self, PyObject *Py_UNUSED(ignored)) {
148+
PyObject *obj = new_native_weakref(2);
149+
if (obj == NULL) {
150+
return NULL;
151+
}
152+
write_marker("created:free\\n");
153+
return obj;
154+
}
155+
136156
static PyMethodDef methods[] = {
137157
{"hold", hold, METH_NOARGS, ""},
158+
{"make", make, METH_NOARGS, ""},
138159
{NULL, NULL, 0, NULL},
139160
};
140161
@@ -173,19 +194,26 @@ def test_native_weakref_shutdown_with_c_retained_object():
173194
if not __graalpython__.is_native:
174195
args += [f'--vm.Dpython.EnableBytecodeDSLInterpreter={str(__graalpython__.is_bytecode_dsl_interpreter).lower()}']
175196
code = textwrap.dedent("""
197+
import gc
176198
import weakref
177199
import native_weakref_shutdown_reproducer_gr50212
178200
179-
obj = native_weakref_shutdown_reproducer_gr50212.hold()
180-
wr = weakref.ref(obj)
201+
held = native_weakref_shutdown_reproducer_gr50212.hold()
202+
free = native_weakref_shutdown_reproducer_gr50212.make()
203+
held_wr = weakref.ref(held)
204+
free_wr = weakref.ref(free)
181205
type_wr = weakref.ref(native_weakref_shutdown_reproducer_gr50212.NativeWeakRef)
182-
print(type(obj).__name__, flush=True)
206+
print(type(held).__name__, type(free).__name__, flush=True)
207+
del free
208+
for _ in range(3):
209+
gc.collect()
210+
print(held_wr() is not None, free_wr() is None, flush=True)
183211
""")
184212
env = dict(ENV)
185213
env["PYTHONPATH"] = os.pathsep.join([module_dir, env["PYTHONPATH"]])
186214
with tempfile.TemporaryDirectory() as tmpdir:
187215
marker = Path(tmpdir) / "deallocated"
188216
env["GR50212_DEALLOC_MARKER"] = str(marker)
189217
proc = subprocess.run([*args, '-c', code], env=env, capture_output=True, text=True, check=True)
190-
assert proc.stdout.strip() == "NativeWeakRef"
191-
assert marker.read_text() == "deallocated\n"
218+
assert proc.stdout.splitlines() == ["NativeWeakRef NativeWeakRef", "True True"]
219+
assert marker.read_text().splitlines() == ["created:held", "created:free", "deallocated:free"]

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/transitions/CApiTransitions.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,11 +1025,11 @@ public static void deallocateNativeWeakRefs(PythonContext pythonContext) {
10251025
Object object = ref != null ? ref.get() : null;
10261026
long refCount = ref != null ? readNativeRefCount(ptr) : 0;
10271027
/*
1028-
* Only native weakrefs with extra C references need shutdown deallocation here.
1029-
* Objects with just MANAGED_REFCNT are still owned by the managed wrapper.
1028+
* Only deallocate weakref referents whose managed wrapper has already been
1029+
* collected and that have no additional native owners. Forcing deallocation while
1030+
* native owners remain can leave dangling references for later shutdown DECREFs.
10301031
*/
1031-
if (refCount > MANAGED_REFCNT && refCount != IMMORTAL_REFCNT &&
1032-
(object == null || !TypeNodes.IsTypeNode.executeUncached(object))) {
1032+
if (object == null && refCount == MANAGED_REFCNT) {
10331033
nativeLookupRemove(context, ptr);
10341034
ptrArray[++idx] = ptr;
10351035
}

0 commit comments

Comments
 (0)