Skip to content

gh-146152: Fix memory leak in _json encoder error paths#146164

Open
okiemute04 wants to merge 15 commits intopython:mainfrom
okiemute04:fix-json-encoder-leak
Open

gh-146152: Fix memory leak in _json encoder error paths#146164
okiemute04 wants to merge 15 commits intopython:mainfrom
okiemute04:fix-json-encoder-leak

Conversation

@okiemute04
Copy link
Contributor

@okiemute04 okiemute04 commented Mar 19, 2026

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:

  • default() raises an exception
  • RecursionError occurs
  • Nested encoding fails

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.
@okiemute04 okiemute04 force-pushed the fix-json-encoder-leak branch from 51c39f9 to 0597100 Compare March 19, 2026 14:00
Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@okiemute04
Copy link
Contributor Author

@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.

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert cosmetic changes. Nothing was properly reverted.

@bedevere-app
Copy link

bedevere-app bot commented Mar 22, 2026

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

- 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
@okiemute04
Copy link
Contributor Author

@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

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not add or remove blank lines randomly.

from test import support
from test.test_json import PyTest, CTest


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave this blank line.

Comment on lines +140 to +143
try:
self.dumps(obj, default=default)
except Exception:
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the infinite recursion pattern + catching the RecursionError explicitly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert cosmetic changes. Nothing was properly reverted.

Modules/_json.c Outdated
}
return -1;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this blank line.

@okiemute04
Copy link
Contributor Author

I have made the requested changes, it's good for another review

@picnixz
Copy link
Member

picnixz commented Mar 22, 2026

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.
@okiemute04
Copy link
Contributor Author

@picnixz all addressed!!! Thanks for the feedback

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1635 to +1643
if (ident != NULL) {
int del_rv = PyDict_DelItem(s->markers, ident);
Py_DECREF(ident);
if (del_rv < 0) {
Py_DECREF(newobj);
return -1;
}

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Comment on lines -1649 to -1653
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part does not need modifications..It was already properly doing its job.

Comment on lines +1654 to +1660
if (ident != NULL) {
int del_rv = PyDict_DelItem(s->markers, ident);
Py_DECREF(ident);
if (del_rv < 0) {
return -1;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can benefit from the same optimization as my other comment as we anyway return -1.

@okiemute04
Copy link
Contributor Author

@picnixz made changes as requested, _json.c Error paths now use (void)PyDict_DelItem; success path unchanged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_json: stale markers entries on error paths in encoder_listencode_obj

3 participants