Repository: ultrajson/ultrajson (language: python)

Issue:
Ref leak when `default` function is called more than once in the serialisation of an object
From https://github.com/ultrajson/ultrajson/issues/278#issuecomment-1091096345:

> The `newObj` is assigned in the `DEFAULT` case in `Object_beginTypeContext` from a call to the `defaultFn`. But after that assignment, there's a `goto BEGIN`. This means the value could potentially get overwritten at a later time, and all but the last one would never get `DECREF`'d. I think the trigger scenario would be the default function again returning an object that can't directly be serialised.

Here's a reproducer that leaks `A(2)` and `A(1)` objects as of ujson 5.2.0 on CPython 3.10.1:

```python3
import gc
import sys
import ujson


class A:
	def __init__(self, value):
		self._value = value

	def foo(self):
		if self._value > 1:
			return A(self._value - 1)
		return 0

	def __repr__(self):
		return f'A({self._value})'


ujson.dumps(A(3), default = lambda x: x.foo())
os = [o for o in gc.get_objects() if type(o).__name__ == 'A']
for o in os:
	print(repr(o), sys.getrefcount(o))
```

Output:

```
A(2) 4
A(1) 4
```


Discussion / hints:

Fix patch:
diff --git a/python/objToJSON.c b/python/objToJSON.c
index 743e6f29..b3d5cc2c 100644
--- a/python/objToJSON.c
+++ b/python/objToJSON.c
@@ -648,6 +648,7 @@ static void Object_beginTypeContext (JSOBJ _obj, JSONTypeContext *tc, JSONObject
     if (newObj)
     {
       PRINTMARK();
+      Py_XDECREF(pc->newObj);
       obj = pc->newObj = newObj;
       level += 1;
       goto BEGIN;


Test patch:
diff --git a/tests/test_ujson.py b/tests/test_ujson.py
index b0b39479..b20404ce 100644
--- a/tests/test_ujson.py
+++ b/tests/test_ujson.py
@@ -974,6 +974,28 @@ def test_issue_334(indent):
     ujson.dumps(a, indent=indent)
 
 
+@pytest.mark.skipif(
+    hasattr(sys, "pypy_version_info"), reason="PyPy uses incompatible GC"
+)
+def test_default_ref_counting():
+    class DefaultRefCountingClass:
+        def __init__(self, value):
+            self._value = value
+
+        def convert(self):
+            if self._value > 1:
+                return type(self)(self._value - 1)
+            return 0
+
+    import gc
+
+    gc.collect()
+    ujson.dumps(DefaultRefCountingClass(3), default=lambda x: x.convert())
+    assert not any(
+        type(o).__name__ == "DefaultRefCountingClass" for o in gc.get_objects()
+    )
+
+
 """
 def test_decode_numeric_int_frc_overflow():
 input = "X.Y"