Fixing memory leak and reference counting bugs, no new release yet.

Signed-off-by: Alexander Dahl <[email protected]>
---
 ...e-to-clear-_user_delete-in-_json_obj.patch |  96 +++++++++
 ...ts-outside-assert-in-json_object_put.patch |  31 +++
 ...et-_userdata-NULL-in-_json_object_ma.patch |  47 +++++
 ...put-to-return-1-for-freed-scalars-an.patch | 195 ++++++++++++++++++
 patches/json-c-0.19/series                    |   7 +
 5 files changed, 376 insertions(+)
 create mode 100644 
patches/json-c-0.19/0001-Issue-934-Be-sure-to-clear-_user_delete-in-_json_obj.patch
 create mode 100644 
patches/json-c-0.19/0002-free-objects-outside-assert-in-json_object_put.patch
 create mode 100644 
patches/json-c-0.19/0003-Issue-934-Also-set-_userdata-NULL-in-_json_object_ma.patch
 create mode 100644 
patches/json-c-0.19/0004-fix-json_object_put-to-return-1-for-freed-scalars-an.patch
 create mode 100644 patches/json-c-0.19/series

diff --git 
a/patches/json-c-0.19/0001-Issue-934-Be-sure-to-clear-_user_delete-in-_json_obj.patch
 
b/patches/json-c-0.19/0001-Issue-934-Be-sure-to-clear-_user_delete-in-_json_obj.patch
new file mode 100644
index 000000000..9d30da84e
--- /dev/null
+++ 
b/patches/json-c-0.19/0001-Issue-934-Be-sure-to-clear-_user_delete-in-_json_obj.patch
@@ -0,0 +1,96 @@
+From: Eric Hawicz <[email protected]>
+Date: Sat, 4 Jul 2026 11:02:06 -0400
+Subject: [PATCH] Issue #934: Be sure to clear _user_delete in
+ _json_object_put_maybe_free() to avoid calling it twice. Add a test to check.
+
+Upstream-Status: Backport [3aa996b5b6bd96c29f2c6da98da33c3fb3d13038]
+---
+ json_object.c             |  1 +
+ json_object.h             |  4 ++--
+ tests/test_deep_nesting.c | 33 +++++++++++++++++++++++++++++++++
+ 3 files changed, 36 insertions(+), 2 deletions(-)
+
+diff --git a/json_object.c b/json_object.c
+index 0a619679bd7e..6cb1bdac86d5 100644
+--- a/json_object.c
++++ b/json_object.c
+@@ -303,6 +303,7 @@ static inline int _json_object_put_maybe_free(struct 
json_object *jso, int free_
+ 
+       if (jso->_user_delete)
+               jso->_user_delete(jso, jso->_userdata);
++      jso->_user_delete = NULL;
+ 
+       switch (jso->o_type)
+       {
+diff --git a/json_object.h b/json_object.h
+index e21138ecf1db..7edb63d4f8cb 100644
+--- a/json_object.h
++++ b/json_object.h
+@@ -259,7 +259,7 @@ JSON_EXPORT void *json_object_get_userdata(json_object 
*jso);
+  * The user_delete parameter is optional and may be passed as NULL, even if
+  * the userdata parameter is non-NULL.  It will be called just before the
+  * json_object is deleted, after it's reference count goes to zero
+- * (see json_object_put()).
++ * (see json_object_put()) but before any child objects are freed.
+  * If this is not provided, it is up to the caller to free the userdata at
+  * an appropriate time. (i.e. after the json_object is deleted)
+  *
+@@ -293,7 +293,7 @@ JSON_EXPORT void json_object_set_userdata(json_object 
*jso, void *userdata,
+  * The user_delete parameter is optional and may be passed as NULL, even if
+  * the userdata parameter is non-NULL.  It will be called just before the
+  * json_object is deleted, after it's reference count goes to zero
+- * (see json_object_put()).
++ * (see json_object_put()) but before any child objects are freed.
+  * If this is not provided, it is up to the caller to free the userdata at
+  * an appropriate time. (i.e. after the json_object is deleted)
+  *
+diff --git a/tests/test_deep_nesting.c b/tests/test_deep_nesting.c
+index 364c4820ea5d..9e251750c480 100644
+--- a/tests/test_deep_nesting.c
++++ b/tests/test_deep_nesting.c
+@@ -56,6 +56,37 @@ static void test_deep_nesting_tostring(const char *str)
+       json_tokener_free(tok);
+ }
+ 
++struct userdata_test {
++      int userdata_val;
++      char *p;
++};
++/*
++ * Check that the user_delete function is only called once, even with the
++ * newer code to avoid deeply nested calls during json_object_put().
++ */
++static void user_delete_test(struct json_object *jso, void *userdata_in)
++{
++      struct userdata_test *userdata = (struct userdata_test *)userdata_in;
++      printf("in user_delete, userdata_val=%d\n", userdata->userdata_val);
++      fflush(stdout);
++      userdata->userdata_val = 0;
++      userdata->p[0] = 'x';
++      userdata->p[8191] = 'x';
++      free(userdata->p);
++}
++static void test_nesting_with_user_delete(void)
++{
++      json_object *jso;
++      struct userdata_test userdata_val = {
++              1, malloc(8192)
++      };
++
++      jso = json_object_new_object();
++      json_object_set_userdata(jso, &userdata_val, user_delete_test);
++      json_object_object_add(jso, "somekey", json_object_new_string("foo"));
++      json_object_put(jso);
++}
++
+ int main(int argc, char **argv)
+ {
+       char *str;      
+@@ -77,5 +108,7 @@ int main(int argc, char **argv)
+ 
+       free(str);
+ 
++      test_nesting_with_user_delete();
++
+       return EXIT_SUCCESS;
+ }
diff --git 
a/patches/json-c-0.19/0002-free-objects-outside-assert-in-json_object_put.patch 
b/patches/json-c-0.19/0002-free-objects-outside-assert-in-json_object_put.patch
new file mode 100644
index 000000000..7601bc68e
--- /dev/null
+++ 
b/patches/json-c-0.19/0002-free-objects-outside-assert-in-json_object_put.patch
@@ -0,0 +1,31 @@
+From: Javid Khan <[email protected]>
+Date: Tue, 30 Jun 2026 18:54:06 +0530
+Subject: [PATCH] free objects outside assert() in json_object_put
+
+Upstream-Status: Backport [f291fa81c6f21d925ea771a8cca597f5886309cc]
+---
+ json_object.c | 7 ++++++-
+ 1 file changed, 6 insertions(+), 1 deletion(-)
+
+diff --git a/json_object.c b/json_object.c
+index 6cb1bdac86d5..71cd25b2a6a9 100644
+--- a/json_object.c
++++ b/json_object.c
+@@ -436,11 +436,16 @@ int json_object_put(struct json_object *jso)
+               // All slots are cleared, now pop back up to the parent
+               {
+                       json_object *parent = jso->_delete_parent;
++                      int rc;
+                       // jso is a child that's already been detached from its 
parent
+                       // so we need to actually free it now
+                       assert(jso->_ref_count == 0);
+                       jso->_ref_count++;   // We're the exclusive owner of 
jso, non-atomic add is ok.
+-                      assert(_json_object_put_maybe_free(jso, 1) == 0);
++                      // Note: the call must not be inside assert(), or it 
gets
++                      // compiled out when NDEBUG is defined and the memory 
leaks.
++                      rc = _json_object_put_maybe_free(jso, 1);
++                      assert(rc == 0);
++                      (void)rc;
+                       jso = parent;
+                       // iteration will be reset at the top of the loop
+               }
diff --git 
a/patches/json-c-0.19/0003-Issue-934-Also-set-_userdata-NULL-in-_json_object_ma.patch
 
b/patches/json-c-0.19/0003-Issue-934-Also-set-_userdata-NULL-in-_json_object_ma.patch
new file mode 100644
index 000000000..b235b00bd
--- /dev/null
+++ 
b/patches/json-c-0.19/0003-Issue-934-Also-set-_userdata-NULL-in-_json_object_ma.patch
@@ -0,0 +1,47 @@
+From: Eric Hawicz <[email protected]>
+Date: Sat, 4 Jul 2026 11:38:15 -0400
+Subject: [PATCH] Issue #934: Also set _userdata=NULL in
+ _json_object_maybe_free() so json_object_put doesn't try to use it as a
+ _delete_parent value. Update the test_deep_nesting test output.
+
+Upstream-Status: Backport [0c3a5a1994ed6b5957d2e878fc1298ce9aa09b3f]
+---
+ json_object.c                    | 7 +++++--
+ tests/test_deep_nesting.expected | 1 +
+ 2 files changed, 6 insertions(+), 2 deletions(-)
+
+diff --git a/json_object.c b/json_object.c
+index 71cd25b2a6a9..095b84e2b4ab 100644
+--- a/json_object.c
++++ b/json_object.c
+@@ -304,6 +304,8 @@ static inline int _json_object_put_maybe_free(struct 
json_object *jso, int free_
+       if (jso->_user_delete)
+               jso->_user_delete(jso, jso->_userdata);
+       jso->_user_delete = NULL;
++      jso->_userdata = NULL; // aka _delete_parent, but json_object_put() will
++                             // have already grabbed it if it needs it.
+ 
+       switch (jso->o_type)
+       {
+@@ -435,10 +437,11 @@ int json_object_put(struct json_object *jso)
+ 
+               // All slots are cleared, now pop back up to the parent
+               {
+-                      json_object *parent = jso->_delete_parent;
+-                      int rc;
+                       // jso is a child that's already been detached from its 
parent
+                       // so we need to actually free it now
++                      // Be sure to grab _delete_parent *before* freeing jso.
++                      json_object *parent = jso->_delete_parent;
++                      int rc;
+                       assert(jso->_ref_count == 0);
+                       jso->_ref_count++;   // We're the exclusive owner of 
jso, non-atomic add is ok.
+                       // Note: the call must not be inside assert(), or it 
gets
+diff --git a/tests/test_deep_nesting.expected 
b/tests/test_deep_nesting.expected
+index b869cfaabe79..b9c1ab98480f 100644
+--- a/tests/test_deep_nesting.expected
++++ b/tests/test_deep_nesting.expected
+@@ -1,2 +1,3 @@
+ Parsed depth 100000 string to json_object: yes
+ Freed json_object
++in user_delete, userdata_val=1
diff --git 
a/patches/json-c-0.19/0004-fix-json_object_put-to-return-1-for-freed-scalars-an.patch
 
b/patches/json-c-0.19/0004-fix-json_object_put-to-return-1-for-freed-scalars-an.patch
new file mode 100644
index 000000000..7e57bad29
--- /dev/null
+++ 
b/patches/json-c-0.19/0004-fix-json_object_put-to-return-1-for-freed-scalars-an.patch
@@ -0,0 +1,195 @@
+From: Arran Cudbard-Bell <[email protected]>
+Date: Tue, 14 Jul 2026 09:32:03 -0400
+Subject: [PATCH] fix json_object_put() to return 1 for freed scalars and empty
+ containers
+
+The iterative rewrite in 17328a67 made json_object_put() return 0 for freed 
scalars, strings and empty containers, contradicting the documented contract in 
json_object.h. Distinguish "freed" from "still referenced" in 
_json_object_put_maybe_free() and add regression tests.
+
+Upstream-Status: Backport [69be99ef8e99d811d7810bdf75864ae800a67e50]
+---
+ json_object.c                    | 49 ++++++++++++++++++++++++++++------------
+ tests/test_deep_nesting.c        | 28 ++++++++++++++++++++++-
+ tests/test_deep_nesting.expected |  7 ++++++
+ 3 files changed, 69 insertions(+), 15 deletions(-)
+
+diff --git a/json_object.c b/json_object.c
+index 095b84e2b4ab..621260885620 100644
+--- a/json_object.c
++++ b/json_object.c
+@@ -273,13 +273,31 @@ struct json_object *json_object_get(struct json_object 
*jso)
+ }
+ 
+ 
++/**
++  * Return values for _json_object_put_maybe_free().
++  * json_object_put_still_refd and json_object_put_freed match the documented
++  * return values of json_object_put(), so they can be returned directly.
++  */
++enum json_object_put_result
++{
++      json_object_put_still_refd = 0, /* refcount decremented, object not 
freed */
++      json_object_put_freed = 1,      /* refcount reached zero, memory 
released */
++      json_object_put_container = 2   /* refcount reached zero, but the 
object is a
++                                         non-empty container: the caller must 
free
++                                         the contents, then the object itself 
*/
++};
++
+ /**
+   * Internal json_object_put function
+-  * Returns 0 if we're done "freeing" the object, either because its memory
+-  * was actually released, or we just needed to decrement the refcount.
+-  * Returns 1 when the object is a non-empty container that still needs to be 
handled.
++  * Returns json_object_put_still_refd if a reference remains, and the object
++  * was not freed.
++  * Returns json_object_put_freed if the object's memory was released, either
++  * because it holds no other objects, or because free_containers was set.
++  * Returns json_object_put_container when the refcount reached zero but the
++  * object is a non-empty container; the caller must free the contents, then
++  * call this function again with free_containers set to free the object 
itself.
+   */
+-static inline int _json_object_put_maybe_free(struct json_object *jso, int 
free_containers)
++static inline enum json_object_put_result _json_object_put_maybe_free(struct 
json_object *jso, int free_containers)
+ {
+       /* Avoid invalid free and crash explicitly instead of (silently)
+        * segfaulting.
+@@ -298,7 +316,7 @@ static inline int _json_object_put_maybe_free(struct 
json_object *jso, int free_
+       if (--jso->_ref_count > 0)
+ #endif
+       {
+-              return 0;  // All done, caller doesn't need to do anything else
++              return json_object_put_still_refd;
+       }
+ 
+       if (jso->_user_delete)
+@@ -315,15 +333,15 @@ static inline int _json_object_put_maybe_free(struct 
json_object *jso, int free_
+                       json_object_object_delete(jso);
+                       break;
+               }
+-              return 1;
+-      case json_type_array: 
++              return json_object_put_container;
++      case json_type_array:
+               // container objects are handled by the caller
+               if (free_containers || 
array_list_length(JC_ARRAY(jso)->c_array) == 0)
+               {
+                       json_object_array_delete(jso);
+                       break;
+               }
+-              return 1;
++              return json_object_put_container;
+       case json_type_string:
+               json_object_string_delete(jso);
+               break;
+@@ -331,16 +349,19 @@ static inline int _json_object_put_maybe_free(struct 
json_object *jso, int free_
+               json_object_generic_delete(jso);
+               break;
+       }
+-      return 0;  // All done, caller doesn't need to do anything else
++      return json_object_put_freed;
+ }
+ 
+ int json_object_put(struct json_object *jso)
+ {
++      enum json_object_put_result rc;
++
+       if (!jso)
+               return 0;
+ 
+-      if (_json_object_put_maybe_free(jso, 0) == 0)
+-              return 0;
++      rc = _json_object_put_maybe_free(jso, 0);
++      if (rc != json_object_put_container)
++              return (int)rc;
+       // else, it's a non-empty container object, handle it below
+ 
+       // Note: jso is now a "zombie" object, _ref_count == 0 but memory not 
yet released
+@@ -399,7 +420,7 @@ int json_object_put(struct json_object *jso)
+                       }
+ 
+                       // Now, handle actually freeing the json_object in that 
slot
+-                      if (!child || _json_object_put_maybe_free(child, 0) == 
0)
++                      if (!child || _json_object_put_maybe_free(child, 0) != 
json_object_put_container)
+                       {
+                               // child is either freed, or still referenced 
somewhere else
+                               // leave it as-is and handle the previous slot
+@@ -441,13 +462,13 @@ int json_object_put(struct json_object *jso)
+                       // so we need to actually free it now
+                       // Be sure to grab _delete_parent *before* freeing jso.
+                       json_object *parent = jso->_delete_parent;
+-                      int rc;
++                      enum json_object_put_result rc;
+                       assert(jso->_ref_count == 0);
+                       jso->_ref_count++;   // We're the exclusive owner of 
jso, non-atomic add is ok.
+                       // Note: the call must not be inside assert(), or it 
gets
+                       // compiled out when NDEBUG is defined and the memory 
leaks.
+                       rc = _json_object_put_maybe_free(jso, 1);
+-                      assert(rc == 0);
++                      assert(rc == json_object_put_freed);
+                       (void)rc;
+                       jso = parent;
+                       // iteration will be reset at the top of the loop
+diff --git a/tests/test_deep_nesting.c b/tests/test_deep_nesting.c
+index 9e251750c480..4bf24578daa1 100644
+--- a/tests/test_deep_nesting.c
++++ b/tests/test_deep_nesting.c
+@@ -81,12 +81,36 @@ static void test_nesting_with_user_delete(void)
+               1, malloc(8192)
+       };
+ 
+-      jso = json_object_new_object();
++      jso = json_object_new_object();
+       json_object_set_userdata(jso, &userdata_val, user_delete_test);
+       json_object_object_add(jso, "somekey", json_object_new_string("foo"));
+       json_object_put(jso);
+ }
+ 
++/*
++ * Check that json_object_put() returns 1 whenever the object is freed,
++ * including for scalars and empty containers, and 0 only when a
++ * reference remains.
++ */
++static void test_put_return_values(void)
++{
++      json_object *jso;
++
++      printf("put(empty object) returned %d\n", 
json_object_put(json_object_new_object()));
++      printf("put(empty array) returned %d\n", 
json_object_put(json_object_new_array()));
++      printf("put(string) returned %d\n", 
json_object_put(json_object_new_string("foo")));
++      printf("put(int) returned %d\n", 
json_object_put(json_object_new_int(42)));
++
++      jso = json_object_new_object();
++      json_object_object_add(jso, "somekey", json_object_new_string("foo"));
++      printf("put(non-empty object) returned %d\n", json_object_put(jso));
++
++      jso = json_object_new_object();
++      json_object_get(jso);
++      printf("put(still-referenced object) returned %d\n", 
json_object_put(jso));
++      printf("put(last reference) returned %d\n", json_object_put(jso));
++}
++
+ int main(int argc, char **argv)
+ {
+       char *str;      
+@@ -110,5 +134,7 @@ int main(int argc, char **argv)
+ 
+       test_nesting_with_user_delete();
+ 
++      test_put_return_values();
++
+       return EXIT_SUCCESS;
+ }
+diff --git a/tests/test_deep_nesting.expected 
b/tests/test_deep_nesting.expected
+index b9c1ab98480f..2449ede3265c 100644
+--- a/tests/test_deep_nesting.expected
++++ b/tests/test_deep_nesting.expected
+@@ -1,3 +1,10 @@
+ Parsed depth 100000 string to json_object: yes
+ Freed json_object
+ in user_delete, userdata_val=1
++put(empty object) returned 1
++put(empty array) returned 1
++put(string) returned 1
++put(int) returned 1
++put(non-empty object) returned 1
++put(still-referenced object) returned 0
++put(last reference) returned 1
diff --git a/patches/json-c-0.19/series b/patches/json-c-0.19/series
new file mode 100644
index 000000000..311299a3a
--- /dev/null
+++ b/patches/json-c-0.19/series
@@ -0,0 +1,7 @@
+# generated by git-ptx-patches
+#tag:base --start-number 1
+0001-Issue-934-Be-sure-to-clear-_user_delete-in-_json_obj.patch
+0002-free-objects-outside-assert-in-json_object_put.patch
+0003-Issue-934-Also-set-_userdata-NULL-in-_json_object_ma.patch
+0004-fix-json_object_put-to-return-1-for-freed-scalars-an.patch
+# 5aff3fd086034e7fb3fdec97d3808ade  - git-ptx-patches magic

base-commit: af7fe92c6398143ffbb7dfb75ae1e6de092cd15c
-- 
2.47.3


Reply via email to