🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
Submitted By: Douglas R. Reno <renodr at linuxfromscratch dot org>
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