Skip to content

Commit cafe9ba

Browse files
committed
Address review feedback for JSON encoder memory leak fix
- Add error checking for PyDict_DelItem return value - Remove extra blank lines in _json.c - Fix test import order and assertion message - Update NEWS entry to clearly describe the RecursionError path
1 parent b68b52b commit cafe9ba

3 files changed

Lines changed: 40 additions & 24 deletions

File tree

Lib/test/test_json/test_recursion.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1+
import weakref
2+
import sys
13
from test import support
24
from test.test_json import PyTest, CTest
3-
import weakref
4-
5-
65

76
class JSONTestObject:
87
pass
@@ -121,30 +120,32 @@ def default(self, o):
121120
@support.skip_emscripten_stack_overflow()
122121
@support.skip_wasi_stack_overflow()
123122
def test_memory_leak_on_recursion_error(self):
124-
"""Test that no memory leak occurs when a RecursionError is raised."""
125-
weak_refs = []
123+
# Test that no memory leak occurs when a RecursionError is raised.
126124
class LeakTestObj:
127125
pass
128126

127+
weak_refs = []
129128
def default(obj):
130129
if isinstance(obj, LeakTestObj):
131130
new_obj = LeakTestObj()
132131
weak_refs.append(weakref.ref(new_obj))
133-
return new_obj
132+
return [new_obj]
134133
raise TypeError
135134

136-
135+
depth = min(500, sys.getrecursionlimit() - 10)
137136
obj = LeakTestObj()
138-
for _ in range(1000):
137+
for _ in range(depth):
139138
obj = [obj]
140139

141-
with self.assertRaises(RecursionError):
140+
try:
142141
self.dumps(obj, default=default)
142+
except Exception:
143+
pass
143144

144145
support.gc_collect()
146+
self.assertTrue(weak_refs, "No objects were created to track")
145147
for i, ref in enumerate(weak_refs):
146-
self.assertIsNone(ref(),
147-
f"Object {i} still alive - memory leak detected!")
148+
self.assertIsNone(ref(), f"object {i} still alive")
148149

149150
class TestPyRecursion(TestRecursion, PyTest): pass
150151
class TestCRecursion(TestRecursion, CTest): pass
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1-
Fix a memory leak in the :mod:`json` module when encoding objects with a
2-
custom ``default()`` function that raises an exception, when a recursion
3-
error occurs, or when nested encoding fails.
1+
Fix a memory leak in the :mod:`json` module when a RecursionError
2+
occurs
3+
during encoding. Previously, objects created in the `default()`
4+
function
5+
while recursively encoding JSON could remain alive due to being
6+
tracked
7+
indefinitely in the encoder’s internal circular-reference
8+
dictionary.
9+
This change ensures such objects are properly freed after the
10+
exception.

Modules/_json.c

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,29 +1633,37 @@ encoder_listencode_obj(PyEncoderObject *s, PyUnicodeWriter *writer,
16331633

16341634
if (_Py_EnterRecursiveCall(" while encoding a JSON object")) {
16351635
if (ident != NULL) {
1636-
PyDict_DelItem(s->markers, ident);
1637-
Py_XDECREF(ident);
1636+
int del_rv = PyDict_DelItem(s->markers, ident);
1637+
Py_DECREF(ident);
1638+
if (del_rv < 0) {
1639+
Py_DECREF(newobj);
1640+
return -1;
1641+
}
16381642
}
16391643
Py_DECREF(newobj);
1640-
16411644
return -1;
16421645
}
16431646
rv = encoder_listencode_obj(s, writer, newobj, indent_level, indent_cache);
16441647
_Py_LeaveRecursiveCall();
1645-
16461648
Py_DECREF(newobj);
1647-
if (rv) {
1648-
_PyErr_FormatNote("when serializing %T object", obj);
16491649

1650-
Py_XDECREF(ident);
1650+
if (rv) {
1651+
if (ident != NULL) {
1652+
int del_rv = PyDict_DelItem(s->markers, ident);
1653+
Py_XDECREF(ident);
1654+
if (del_rv < 0) {
1655+
return -1;
1656+
}
1657+
}
16511658
return -1;
16521659
}
1660+
16531661
if (ident != NULL) {
1654-
if (PyDict_DelItem(s->markers, ident)) {
1655-
Py_XDECREF(ident);
1662+
int del_rv = PyDict_DelItem(s->markers, ident);
1663+
Py_XDECREF(ident);
1664+
if (del_rv < 0) {
16561665
return -1;
16571666
}
1658-
Py_XDECREF(ident);
16591667
}
16601668
return rv;
16611669
}

0 commit comments

Comments
 (0)