Skip to content

Conversation

@VanshAgarwal24036
Copy link
Contributor

Summary

When JSONDecodeError is user-replaced and re-enters during JSON parsing,
_raise_errmsg could reuse a freed exception type, leading to a
use-after-free.

This change holds a strong reference across the call and validates the
exception type before setting it, falling back safely when needed.

Issue

@VanshAgarwal24036
Copy link
Contributor Author

skip news

@ZeroIntensity
Copy link
Member

No, this affects user-facing APIs; please add a news entry.

Copy link
Member

@ZeroIntensity ZeroIntensity left a comment

Choose a reason for hiding this comment

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

  1. Please avoid making unnecessary code changes. They make it significantly more difficult to review.
  2. This is doing too much. The much simpler and more correct fix would be to simply move the Py_DECREF call on line 426 to after the if (exc) block.
  3. Please add a test case and news entry.

@bedevere-app
Copy link

bedevere-app bot commented Jan 8, 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.

@VanshAgarwal24036
Copy link
Contributor Author

Thanks for the review.

I’ve updated the patch to keep the fix minimal, added a regression test
that restores global state, and included a NEWS entry as requested.

Modules/_json.c Outdated
Comment on lines 418 to 420

PyObject *JSONDecodeError =
PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
Copy link
Member

Choose a reason for hiding this comment

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

Again please don't make unrelated formatting changes.

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

Modules/_json.c Outdated
Comment on lines 427 to 426
if (exc) {
PyObject *exc = PyObject_CallFunction(JSONDecodeError, "zOn", msg, s, end);
if (exc != NULL) {
Copy link
Member

Choose a reason for hiding this comment

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

Same here. The only change we need is the movement of the Py_DECREF call.

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 change, only move Py_DECREF(JSONDecodeError);.

@VanshAgarwal24036
Copy link
Contributor Author

Thanks for the clarification. I’ve updated the test to only assert that the
interpreter doesn’t crash and pushed the changes.

@VanshAgarwal24036
Copy link
Contributor Author

The NEWS entry was removed per discussion above. Could someone please add the “skip news” label?

@VanshAgarwal24036
Copy link
Contributor Author

I have made the requested changes; please review again.

@bedevere-app
Copy link

bedevere-app bot commented Jan 9, 2026

Thanks for making the requested changes!

@ZeroIntensity: please review the changes made to this pull request.

Modules/_json.c Outdated
Comment on lines 418 to 420

PyObject *JSONDecodeError =
PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
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 change.

Modules/_json.c Outdated
Comment on lines 427 to 426
if (exc) {
PyObject *exc = PyObject_CallFunction(JSONDecodeError, "zOn", msg, s, end);
if (exc != NULL) {
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 change, only move Py_DECREF(JSONDecodeError);.


def test_reentrant_jsondecodeerror_does_not_crash(self):
# gh-143544
import json
Copy link
Member

Choose a reason for hiding this comment

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

Please move this import at the module top level.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved import json to the module top level as requested. Thanks.

Modules/_json.c Outdated
Py_DECREF(exc);
}

/* Move DECREF after PyErr_SetObject */
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that this comment is useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the comment as suggested.

):
# The exact exception type is not important here;
# this test only ensures we don't crash.
with self.assertRaises(Exception):
Copy link
Member

Choose a reason for hiding this comment

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

Check the exact exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point — agreed. I’ll update the test to assert the exact expected exception (JSONDecodeError) instead of a generic Exception, so the regression is checked more precisely.

hook = Trigger()
with (
support.swap_attr(json, "JSONDecodeError", hook),
support.swap_attr(json.decoder, "JSONDecodeError", hook)
Copy link
Member

Choose a reason for hiding this comment

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

Using swap_attr() context manager keeps hook alive which prevents triggering the crash. Please move back to regular assignment using try/finally.

Also, you need to clear hook (ex: del hook) after the assignment to trigger the crash.

Currently, the test doesn't crash if I revert the _json.c fix.

Copy link
Member

Choose a reason for hiding this comment

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

Using swap_attr() context manager keeps hook alive which prevents triggering the crash. Please move back to regular assignment using try/finally.

Oh! that's my bad.

Copy link
Member

Choose a reason for hiding this comment

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

However, a comment may be needed so that others like me don't forget.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the clarification — that makes sense.
I’ll revert swap_attr() usage, restore try/finally, and explicitly del hook after assignment so the test actually reproduces the original crash when _json.c is reverted.
I’ll update the test accordingly.

Copy link
Member

Choose a reason for hiding this comment

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

However, a comment may be needed so that others like me don't forget.

Maybe the test should check the reference count: self.assertEqual(sys.getrefcount(hook), 3) before del hook.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point — I’ve added a comment explaining why del hook is required and an explicit sys.getrefcount() assertion before deletion to document the reference state.
This should make the intent of the test clearer for future readers.

@@ -1,4 +1,5 @@
from test.test_json import PyTest, CTest
import json
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure this is the right place for such test. This file tests how different decoding errors are handled (and some encoding errors, but I think they are misplaced). But we need to test the bug in the code that raises JSONDecodeError. Maybe test_speedups.py is better place for this.

# Remove JSONDecodeError during construction to trigger re-entrancy
del json.JSONDecodeError
del json.decoder.JSONDecodeError
return ValueError("boom")
Copy link
Member

Choose a reason for hiding this comment

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

Just for the case, call test.support.gc_collect() .

del json.decoder.JSONDecodeError
return ValueError("boom")

hook = Trigger()
Copy link
Member

Choose a reason for hiding this comment

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

hook must be a subclass of BaseException, otherwise PyErr_SetObject() will raise a SystemError.

class hook(Exception):
    def __new__(...):
        # ...

And now calling gc_collect() is a necessity, because class creates a reference loop.

@VanshAgarwal24036
Copy link
Contributor Author

@serhiy-storchaka Thanks for the feedback.

I placed the test in test_fail.py because the regression manifests while raising JSONDecodeError during decoding, and this file already exercises failure paths around decoding errors.

That said, I’m happy to move the test to test_speedups.py or another more appropriate location if you think that’s a better fit — please let me know your preference.

@VanshAgarwal24036
Copy link
Contributor Author

I have made the requested changes; please review again.

@bedevere-app
Copy link

bedevere-app bot commented Jan 10, 2026

Thanks for making the requested changes!

@picnixz, @ZeroIntensity: please review the changes made to this pull request.

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.

There is no good place for such test, but I think test_speedups is less wrong than test_fail.

But I suspect that it may be impossible to write a test without raising SystemError for the fixed code, which is only slightly better than a crash. If this is so, we will give up and accept a fix without unit test.

raise self

hook = Trigger()
hook = Trigger("boom")
Copy link
Member

Choose a reason for hiding this comment

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

No, hook should be not an instance of exception, but an exception class itself.

So, you need to define __new__() to trigger execution when it is called, not __call__().

self.assertEqual(sys.getrefcount(hook), 3)
del hook

support.gc_collect()
Copy link
Member

Choose a reason for hiding this comment

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

It does not have effect here, because there are references like json.decoder.JSONDecodeError.

But calling it after del json.decoder.JSONDecodeError in __new__() will also not help, because we still have a reference as a method parameter. So, it is useless.

Maybe we could not create a good test for this issue.

@VanshAgarwal24036
Copy link
Contributor Author

Thanks for clarifying. I agree that reliably testing this without triggering a SystemError may not be feasible.
If you’re OK with proceeding without a unit test, I can drop the test changes and keep the fix only. Please let me know how you’d prefer to move forward.

@serhiy-storchaka
Copy link
Member

If this is not feasible, drop the tests.

Thank you for your PR.

@VanshAgarwal24036
Copy link
Contributor Author

VanshAgarwal24036 commented Jan 10, 2026

I’ve dropped the test as suggested since it doesn’t seem feasible to reliably reproduce this case without introducing a SystemError.
All CI checks are now passing.

Could you please re-review and let me know if anything else is needed?

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.

5 participants