gh-146152: Fix memory leak in _json encoder error paths#146164
gh-146152: Fix memory leak in _json encoder error paths#146164okiemute04 wants to merge 15 commits intopython:mainfrom
Conversation
3eddfdf to
51c39f9
Compare
Only clean up markers on the RecursionError path (PATH B), where objects accumulate during stack unwinding. Other error paths are safe because the markers dict is local and will be freed. Based on review feedback from @raminfp and @serhiy-storchaka.
51c39f9 to
0597100
Compare
serhiy-storchaka
left a comment
There was a problem hiding this comment.
I am not sure that this works. Please add a test.
- Define
default()which creates a new object, adds a weak reference to it to the list, and returns that object. - Call JSON encoding, and after catching an exception, call
test.support.gc_collect(), then test that all weak references in the list return None.
|
@serhiy-storchaka Thanks for the review! I've added a test to test_recursion.py that creates objects tracked with weak references, triggers a RecursionError, and verifies all objects are freed after garbage collection. |
serhiy-storchaka
left a comment
There was a problem hiding this comment.
Thank you for your update, @okiemute04.
The test does not fail with the current code. I am not sure it tests what it should test.
Modules/_json.c
Outdated
| Py_DECREF(newobj); | ||
| if (rv) { | ||
| _PyErr_FormatNote("when serializing %T object", obj); | ||
|
|
There was a problem hiding this comment.
Revert cosmetic changes. Nothing was properly reverted.
Misc/NEWS.d/next/Library/2026-03-19-04-18-27.gh-issue-146152.Ifrd7O.rst
Outdated
Show resolved
Hide resolved
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
- 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
…/cpython into fix-json-encoder-leak
|
@picnixz has addressed your feedback and added error checking for PyDict_DelItem return value, removed extra blank lines in _json.c, fixed import order and assertion message format in test, updated NEWS entry to clearly describe the RecursionError path...thanks once again for the guide, really appreciate |
picnixz
left a comment
There was a problem hiding this comment.
Please do not add or remove blank lines randomly.
| from test import support | ||
| from test.test_json import PyTest, CTest | ||
|
|
||
|
|
Lib/test/test_json/test_recursion.py
Outdated
| try: | ||
| self.dumps(obj, default=default) | ||
| except Exception: | ||
| pass |
There was a problem hiding this comment.
Why not use the infinite recursion pattern + catching the RecursionError explicitly?
There was a problem hiding this comment.
okay, that's a good suggestion, it makes more sense, I will do that
Modules/_json.c
Outdated
| Py_DECREF(newobj); | ||
| if (rv) { | ||
| _PyErr_FormatNote("when serializing %T object", obj); | ||
|
|
There was a problem hiding this comment.
Revert cosmetic changes. Nothing was properly reverted.
Modules/_json.c
Outdated
| } | ||
| return -1; | ||
| } | ||
|
|
|
I have made the requested changes, it's good for another review |
|
Do not ask for a review if tests are failing please. |
- Use support.infinite_recursion() context manager - Catch RecursionError explicitly instead of all exceptions - Fix import order with two blank lines after from imports - Remove extra blank lines in _json.c - Use Py_DECREF instead of Py_XDECREF
…/cpython into fix-json-encoder-leak # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
|
@picnixz all addressed!!! Thanks for the feedback |
picnixz
left a comment
There was a problem hiding this comment.
I will warn it once. I repeated this many times. But do not change unrelates lines and review what your LLM is giving you. I do not have time to lose with unfiltered LLM output.
| if (ident != NULL) { | ||
| int del_rv = PyDict_DelItem(s->markers, ident); | ||
| Py_DECREF(ident); | ||
| if (del_rv < 0) { | ||
| Py_DECREF(newobj); | ||
| return -1; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
| if (ident != NULL) { | |
| int del_rv = PyDict_DelItem(s->markers, ident); | |
| Py_DECREF(ident); | |
| if (del_rv < 0) { | |
| Py_DECREF(newobj); | |
| return -1; | |
| } | |
| } | |
| if (ident != NULL) { | |
| (void)PyDict_DelItem(s->markers, ident); | |
| } |
Actually since we are anyway falling through the return -1 case we can simplify this as follows. Then keep the XDECREF for indent. However explicitly suppress the error code of DelItem.
| if (PyDict_DelItem(s->markers, ident)) { | ||
| Py_XDECREF(ident); | ||
| int del_rv = PyDict_DelItem(s->markers, ident); | ||
| Py_DECREF(ident); | ||
| if (del_rv < 0) { | ||
| return -1; | ||
| } | ||
| Py_XDECREF(ident); |
There was a problem hiding this comment.
This part does not need modifications..It was already properly doing its job.
| if (ident != NULL) { | ||
| int del_rv = PyDict_DelItem(s->markers, ident); | ||
| Py_DECREF(ident); | ||
| if (del_rv < 0) { | ||
| return -1; | ||
| } | ||
| } |
There was a problem hiding this comment.
This one can benefit from the same optimization as my other comment as we anyway return -1.
|
@picnixz made changes as requested, _json.c Error paths now use (void)PyDict_DelItem; success path unchanged |
Fixes #146152
Add PyDict_DelItem calls to remove objects from the markers dict
on all error paths in encoder_listencode_obj. Previously, objects
were only removed on the success path, causing memory leaks when:
_json: stale markers entries on error paths inencoder_listencode_obj#146152