Submitted By: Douglas R. Reno Date: 2025-11-20 Initial Package Version: 1.14.5 Origin: Upstream (commits 372d9bc6b, 2fc439b7d, 2fc439b7d) Upstream Status: Applied Description: Fixes several known issues with SWIG 4.4.0 and Python 3.14, including memory leaks and crashes. diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/include/svn_types.swg subversion-1.14.5/subversion/bindings/swig/include/svn_types.swg --- subversion-1.14.5.orig/subversion/bindings/swig/include/svn_types.swg 2024-11-22 01:56:17.000000000 -0600 +++ subversion-1.14.5/subversion/bindings/swig/include/svn_types.swg 2025-11-19 20:24:40.999905161 -0600 @@ -601,7 +601,7 @@ svn_ ## TYPE ## _swig_rb_closed(VALUE se %typemap(in, noblock=1) apr_pool_t * { /* Verify that the user supplied a valid pool */ if ($input != Py_None && $input != _global_py_pool) { - SWIG_Python_TypeError(SWIG_TypePrettyName($descriptor), $input); + SWIG_Error(SWIG_TypeError, "a '$type' is expected"); SWIG_arg_fail($svn_argnum); SWIG_fail; } diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/python/tests/mergeinfo.py subversion-1.14.5/subversion/bindings/swig/python/tests/mergeinfo.py --- subversion-1.14.5.orig/subversion/bindings/swig/python/tests/mergeinfo.py 2019-11-03 23:59:36.000000000 -0600 +++ subversion-1.14.5/subversion/bindings/swig/python/tests/mergeinfo.py 2025-11-19 20:24:35.837351707 -0600 @@ -18,7 +18,7 @@ # under the License. # # -import unittest, os, sys, gc +import unittest, os, sys, weakref, gc from svn import core, repos, fs import utils @@ -125,6 +125,9 @@ class SubversionMergeinfoTestCase(unitte } self.compare_mergeinfo_catalogs(mergeinfo, expected_mergeinfo) + @unittest.skipIf(utils.HAS_DEFERRED_REFCOUNT, + "Reference counting tests skipped because of deferred " + "reference counting") def test_mergeinfo_leakage__incorrect_range_t_refcounts(self): """Ensure that the ref counts on svn_merge_range_t objects returned by svn_mergeinfo_parse() are correct.""" @@ -138,7 +141,8 @@ class SubversionMergeinfoTestCase(unitte # ....and now 3 (incref during iteration of each range object) refcount = sys.getrefcount(r) - # ....and finally, 4 (getrefcount() also increfs) + # ....and finally, 4 (getrefcount() also increfs, unless deferred + # reference counting) expected = 4 # Note: if path and index are not '/trunk' and 0 respectively, then @@ -150,8 +154,49 @@ class SubversionMergeinfoTestCase(unitte "cause: incorrect Py_INCREF/Py_DECREF usage in libsvn_swig_py/" "swigutil_py.c." % (expected, refcount, path, i))) + def test_mergeinfo_leakage__incorrect_range_t_weakrefs(self): + """Ensure that the ref counts on svn_merge_range_t objects returned by + svn_mergeinfo_parse() are correct.""" + # When reference counting is working properly, each svn_merge_range_t in + # the returned mergeinfo will have a ref count of 1... + mergeinfo = core.svn_mergeinfo_parse(self.TEXT_MERGEINFO1) + merge_range_refdict = weakref.WeakValueDictionary() + merge_range_indexes = [] + n_merge_range = 0 + for (path, rangelist) in core._as_list(mergeinfo.items()): + # ....and now 2 (incref during iteration of rangelist) + + for (i, r) in enumerate(rangelist): + # ....and now 3 (incref during iteration of each range object) + + idx = (path, i) + merge_range_refdict[idx] = r + merge_range_indexes.append(idx) + n_merge_range += 1 + + # Note: if path and index are not '/trunk' and 0 respectively, then + # only some of the range objects are leaking, which is, as far as + # leaks go, even more impressive. + + del rangelist, r + gc.collect() + # Now (strong) reference count of all svn_merge_range_t should be 1 + # again and those objects should not be removed yet. + for idx in merge_range_indexes: + self.assertIn(idx, merge_range_refdict, ( + "Refarence count error on svn_merge_info_t object for " + "(path: %s, index: %d). It should still exists because " + "mergeinfo holds its reference, but after GC, it already " + "removed." % idx)) del mergeinfo gc.collect() + if merge_range_refdict: + # certainly memory leak, but we want to listing up leaked objects + # before raise an assertion error. + self.assertFalse(merge_range_refdict, + "Memory leak! All svn_merge_range_t object holded " + "by mergeinfo object should be removed, but at least " + "one object still alive.") def test_mergeinfo_leakage__lingering_range_t_objects_after_del(self): """Ensure that there are no svn_merge_range_t objects being tracked by @@ -162,6 +207,9 @@ class SubversionMergeinfoTestCase(unitte objects will be garbage collected and thus, not appear in the list of objects returned by gc.get_objects().""" mergeinfo = core.svn_mergeinfo_parse(self.TEXT_MERGEINFO1) + lingering = get_svn_merge_range_t_objects() + self.assertNotEqual(lingering, list()) + del lingering del mergeinfo gc.collect() lingering = get_svn_merge_range_t_objects() diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/python/tests/repository.py subversion-1.14.5/subversion/bindings/swig/python/tests/repository.py --- subversion-1.14.5.orig/subversion/bindings/swig/python/tests/repository.py 2024-01-19 22:00:04.000000000 -0600 +++ subversion-1.14.5/subversion/bindings/swig/python/tests/repository.py 2025-11-19 20:24:35.837802341 -0600 @@ -87,15 +87,32 @@ class DumpStreamParser(repos.ParseFns3): class BatonCollector(repos.ChangeCollector): """A ChangeCollector with collecting batons, too""" + def __init__(self, fs_ptr, root, pool=None, notify_cb=None): + + def get_expected_baton_refcount(): + """determine expected refcount of batons within a batoun_tuple, + by using dumy object""" + self.open_root(-1, None) + for baton_tuple in self.batons: + rc = sys.getrefcount(baton_tuple[2]) + break + return rc + repos.ChangeCollector.__init__(self, fs_ptr, root, pool, notify_cb) - self.batons = [] self.close_called = False self.abort_called = False + # temporary values for get_expected_baton_refcount + self.batons = [] + self.expected_baton_refcount = 0 + # determin expected_baton_refcount + self.expected_baton_refcount = get_expected_baton_refcount() + # re-initialize the values after calling get_expected_baton_refcount() + self.batons = [] def open_root(self, base_revision, dir_pool=None): bt = repos.ChangeCollector.open_root(self, base_revision, dir_pool) - self.batons.append((b'dir baton', b'', bt, sys.getrefcount(bt))) + self.batons.append((b'dir baton', b'', bt, self.expected_baton_refcount)) return bt def add_directory(self, path, parent_baton, @@ -104,14 +121,14 @@ class BatonCollector(repos.ChangeCollect copyfrom_path, copyfrom_revision, dir_pool) - self.batons.append((b'dir baton', path, bt, sys.getrefcount(bt))) + self.batons.append((b'dir baton', path, bt, self.expected_baton_refcount)) return bt def open_directory(self, path, parent_baton, base_revision, dir_pool=None): bt = repos.ChangeCollector.open_directory(self, path, parent_baton, base_revision, dir_pool) - self.batons.append((b'dir baton', path, bt, sys.getrefcount(bt))) + self.batons.append((b'dir baton', path, bt, self.expected_baton_refcount)) return bt def add_file(self, path, parent_baton, @@ -119,13 +136,13 @@ class BatonCollector(repos.ChangeCollect bt = repos.ChangeCollector.add_file(self, path, parent_baton, copyfrom_path, copyfrom_revision, file_pool) - self.batons.append((b'file baton', path, bt, sys.getrefcount(bt))) + self.batons.append((b'file baton', path, bt, self.expected_baton_refcount)) return bt def open_file(self, path, parent_baton, base_revision, file_pool=None): bt = repos.ChangeCollector.open_file(self, path, parent_baton, base_revision, file_pool) - self.batons.append((b'file baton', path, bt, sys.getrefcount(bt))) + self.batons.append((b'file baton', path, bt, self.expected_baton_refcount)) return bt def close_edit(self, pool=None): @@ -429,29 +446,33 @@ class SubversionRepositoryTestCase(unitt root = fs.revision_root(self.fs, self.rev) editor = BatonCollector(self.fs, root) e_ptr, e_baton = delta.make_editor(editor) + refcount_at_first = sys.getrefcount(e_ptr) repos.replay(root, e_ptr, e_baton) - for baton in editor.batons: - self.assertEqual(sys.getrefcount(baton[2]), 2, + for baton_tuple in editor.batons: + # baton_tuple: 4-tuple(baton_type: bytes, node: bytes, bt: baton, + # expected_refcount_of_bt: int) + self.assertEqual(sys.getrefcount(baton_tuple[2]), baton_tuple[3], "leak on baton %s after replay without errors" - % repr(baton)) + % repr(baton_tuple)) del e_baton - self.assertEqual(sys.getrefcount(e_ptr), 2, + self.assertEqual(sys.getrefcount(e_ptr), refcount_at_first, "leak on editor baton after replay without errors") editor = BatonCollectorErrorOnClose(self.fs, root, error_path=b'branches/v1x') e_ptr, e_baton = delta.make_editor(editor) + refcount_at_first = sys.getrefcount(e_ptr) self.assertRaises(SubversionException, repos.replay, root, e_ptr, e_baton) batons = editor.batons # As svn_repos_replay calls neither close_edit callback nor abort_edit # if an error has occured during processing, references of Python objects # in decendant batons may live until e_baton is deleted. del e_baton - for baton in batons: - self.assertEqual(sys.getrefcount(baton[2]), 2, + for baton_tuple in batons: + self.assertEqual(sys.getrefcount(baton_tuple[2]), baton_tuple[3], "leak on baton %s after replay with an error" - % repr(baton)) - self.assertEqual(sys.getrefcount(e_ptr), 2, + % repr(baton_tuple)) + self.assertEqual(sys.getrefcount(e_ptr), refcount_at_first, "leak on editor baton after replay with an error") def test_delta_editor_apply_textdelta_handler_refcount(self): diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/python/tests/utils.py subversion-1.14.5/subversion/bindings/swig/python/tests/utils.py --- subversion-1.14.5.orig/subversion/bindings/swig/python/tests/utils.py 2022-03-27 09:53:32.000000000 -0500 +++ subversion-1.14.5/subversion/bindings/swig/python/tests/utils.py 2025-11-19 20:24:35.838127789 -0600 @@ -95,3 +95,13 @@ def codecs_eq(a, b): def is_defaultencoding_utf8(): return codecs_eq(sys.getdefaultencoding(), 'utf-8') + +def get_holded_refcount_by_getrefcount(): + "get refcount holded by sys.getrefcount() if its arg is a local variable" + a = [] + rv = sys.getrefcount(a) - 1 + return rv + +HAS_DEFERRED_REFCOUNT = not get_holded_refcount_by_getrefcount() + +del get_holded_refcount_by_getrefcount diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/svn_delta.i subversion-1.14.5/subversion/bindings/swig/svn_delta.i --- subversion-1.14.5.orig/subversion/bindings/swig/svn_delta.i 2023-10-19 23:00:04.000000000 -0500 +++ subversion-1.14.5/subversion/bindings/swig/svn_delta.i 2025-11-19 20:24:38.850833031 -0600 @@ -208,6 +208,7 @@ void _ops_get(int *num_ops, const svn_tx # Baton container class for editor/parse_fns3 batons and their decendants. class _ItemBaton: def __init__(self, editor, pool, baton=None): + import libsvn.core self.pool = pool if pool else libsvn.core.svn_pool_create() self.baton = baton self.editor = editor