Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the C extension module to harden is_namedtuple. #284

Merged
merged 5 commits into from Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.txt
@@ -1,3 +1,11 @@
Version 3.17.5 released 2021-08-XX

* Fix the C extension module to harden is_namedtuple against looks-a-likes such
as Mocks. Also prevent dict encoding from causing an unraised SystemError when
encountering a non-Dict. Noticed by running user tests against a CPython
interpreter with C asserts enabled (COPTS += -UNDEBUG).
https://github.com/simplejson/simplejson/pull/284

Version 3.17.4 released 2021-08-19

* Upgrade cibuildwheel
Expand Down
11 changes: 11 additions & 0 deletions simplejson/_speedups.c
Expand Up @@ -386,6 +386,8 @@ static int
_is_namedtuple(PyObject *obj)
{
int rval = 0;
/* We intentionally accept anything with a duck typed _asdict method rather
* than requiring it to pass PyTuple_Check(obj). */
PyObject *_asdict = PyObject_GetAttrString(obj, "_asdict");
if (_asdict == NULL) {
PyErr_Clear();
Expand Down Expand Up @@ -2853,6 +2855,15 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss
return rv;
newobj = PyObject_CallMethod(obj, "_asdict", NULL);
if (newobj != NULL) {
if (!PyDict_Check(newobj)) {
PyErr_Format(
PyExc_TypeError,
"_asdict() must return a dict, not %.80s",
Py_TYPE(newobj)->tp_name
);
Py_DECREF(newobj);
return -1;
}
rv = encoder_listencode_dict(s, rval, newobj, indent_level);
Py_DECREF(newobj);
}
Expand Down
27 changes: 27 additions & 0 deletions simplejson/tests/test_namedtuple.py
Expand Up @@ -3,6 +3,11 @@
import simplejson as json
from simplejson.compat import StringIO

try:
from unittest import mock
except ImportError:
mock = None

try:
from collections import namedtuple
except ImportError:
Expand Down Expand Up @@ -120,3 +125,25 @@ def test_asdict_not_callable_dumps(self):
self.assertEqual(
json.dumps(f({})),
json.dumps(f(DeadDict()), namedtuple_as_object=True))

def test_asdict_does_not_return_dict(self):
if not mock:
if hasattr(unittest, "SkipTest"):
raise unittest.SkipTest("unittest.mock required")
else:
print("unittest.mock not available")
return
fake = mock.Mock()
self.assertTrue(hasattr(fake, '_asdict'))
self.assertTrue(callable(fake._asdict))
self.assertFalse(isinstance(fake._asdict(), dict))
# https://github.com/simplejson/simplejson/pull/284
# when running under a debug build of CPython (COPTS=-UNDEBUG)
# a C assertion could fire due to an unchecked error of an PyDict
# API call on a non-dict internally in _speedups.c. Without a debug
# build of CPython this test likely passes either way despite the
# potential for internal data corruption. Getting it to crash in
# a debug build is not always easy either as it requires an
# assert(!PyErr_Occurred()) that could fire later on.
with self.assertRaises(TypeError):
json.dumps({23: fake}, namedtuple_as_object=True, for_json=False)