Skip to content

Commit fe86852

Browse files
committed
Prevent unregistering a filter that's still in use in a FilterList
1 parent ac25a44 commit fe86852

File tree

6 files changed

+86
-41
lines changed

6 files changed

+86
-41
lines changed

pygit2/__init__.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@
286286
_cache_enums,
287287
discover_repository,
288288
filter_register,
289-
filter_unregister,
290289
hash,
291290
hashfile,
292291
init_file_backend,
@@ -545,6 +544,27 @@ def clone_repository(
545544
return Repository._from_c(crepo[0], owned=True)
546545

547546

547+
def filter_unregister(name: str) -> None:
548+
"""
549+
Unregister the given filter.
550+
551+
Note that the filter registry is not thread safe. Any registering or
552+
deregistering of filters should be done outside of any possible usage
553+
of the filters.
554+
555+
In particular, any FilterLists that use the filter must have been garbage
556+
collected before you can unregister the filter.
557+
"""
558+
from .filter import FilterList
559+
560+
if FilterList._is_filter_in_use(name):
561+
raise RuntimeError(f"filter still in use: '{name}'")
562+
563+
c_name = to_bytes(name)
564+
err = C.git_filter_unregister(c_name)
565+
check_error(err)
566+
567+
548568
tree_entry_key = functools.cmp_to_key(tree_entry_cmp)
549569

550570
settings = Settings()
@@ -610,7 +630,6 @@ def clone_repository(
610630
# Low Level API (not present in .pyi)
611631
'FilterSource',
612632
'filter_register',
613-
'filter_unregister',
614633
'GIT_APPLY_LOCATION_BOTH',
615634
'GIT_APPLY_LOCATION_INDEX',
616635
'GIT_APPLY_LOCATION_WORKDIR',

pygit2/_pygit2.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,5 @@ def reference_is_valid_name(refname: str) -> bool: ...
857857
def tree_entry_cmp(a: Object, b: Object) -> int: ...
858858
def _cache_enums() -> None: ...
859859
def filter_register(name: str, filter: type[Filter]) -> None: ...
860-
def filter_unregister(name: str) -> None: ...
861860

862861
_OidArg = str | Oid

pygit2/decl/filter.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ typedef enum {
1111
GIT_FILTER_ATTRIBUTES_FROM_COMMIT = ...,
1212
} git_filter_flag_t;
1313

14+
int git_filter_unregister(
15+
const char *name);
16+
1417
int git_filter_list_load(
1518
git_filter_list **filters,
1619
git_repository *repo,

pygit2/filter.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
2424
# Boston, MA 02110-1301, USA.
2525

26+
from __future__ import annotations
27+
28+
import weakref
2629
from collections.abc import Callable
2730
from typing import TYPE_CHECKING
2831

@@ -116,16 +119,34 @@ def close(self, write_next: Callable[[bytes], None]) -> None:
116119

117120

118121
class FilterList:
122+
_all_filter_lists: set[weakref.ReferenceType[FilterList]] = set()
123+
119124
_pointer: GitFilterListC
120125

121126
@classmethod
122127
def _from_c(cls, ptr: GitFilterListC):
123128
if ptr == ffi.NULL:
124129
return None
130+
125131
fl = cls.__new__(cls)
126132
fl._pointer = ptr
133+
134+
# Keep track of this FilterList until it's garbage collected. This lets
135+
# `filter_unregister` ensure the user isn't trying to delete a filter
136+
# that's still in use.
137+
ref = weakref.ref(fl, FilterList._all_filter_lists.remove)
138+
FilterList._all_filter_lists.add(ref)
139+
127140
return fl
128141

142+
@classmethod
143+
def _is_filter_in_use(cls, name: str) -> bool:
144+
for ref in cls._all_filter_lists:
145+
fl = ref()
146+
if fl is not None and name in fl:
147+
return True
148+
return False
149+
129150
def __contains__(self, name: str) -> bool:
130151
if not isinstance(name, str):
131152
raise TypeError('argument must be str')

src/pygit2.c

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -345,31 +345,6 @@ filter_register(PyObject *self, PyObject *args, PyObject *kwds)
345345
return result;
346346
}
347347

348-
PyDoc_STRVAR(filter_unregister__doc__,
349-
"filter_unregister(name: str) -> None\n"
350-
"\n"
351-
"Unregister the given filter.\n"
352-
"\n"
353-
"Note that the filter registry is not thread safe. Any registering or\n"
354-
"deregistering of filters should be done outside of any possible usage\n"
355-
"of the filters.\n");
356-
357-
PyObject *
358-
filter_unregister(PyObject *self, PyObject *args)
359-
{
360-
const char *name;
361-
Py_ssize_t size;
362-
int err;
363-
364-
if (!PyArg_ParseTuple(args, "s#", &name, &size))
365-
return NULL;
366-
if ((err = git_filter_unregister(name)) < 0)
367-
return Error_set(err);
368-
369-
Py_RETURN_NONE;
370-
}
371-
372-
373348
static void
374349
forget_enums(void)
375350
{
@@ -441,7 +416,6 @@ PyMethodDef module_methods[] = {
441416
{"reference_is_valid_name", reference_is_valid_name, METH_O, reference_is_valid_name__doc__},
442417
{"tree_entry_cmp", tree_entry_cmp, METH_VARARGS, tree_entry_cmp__doc__},
443418
{"filter_register", (PyCFunction)filter_register, METH_VARARGS | METH_KEYWORDS, filter_register__doc__},
444-
{"filter_unregister", filter_unregister, METH_VARARGS, filter_unregister__doc__},
445419
{"_cache_enums", _cache_enums, METH_NOARGS, _cache_enums__doc__},
446420
{NULL}
447421
};

test/test_filter.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import codecs
2+
import gc
23
from collections.abc import Callable, Generator
34
from io import BytesIO
45

@@ -56,32 +57,34 @@ class _UnmatchedFilter(_Rot13Filter):
5657
attributes = 'filter=rot13'
5758

5859

60+
def _filter_fixture(name: str, filter: type[Filter]) -> Generator[None, None, None]:
61+
pygit2.filter_register(name, filter)
62+
yield
63+
64+
# Collect any FilterLists that may use this filter before unregistering it
65+
gc.collect()
66+
67+
pygit2.filter_unregister(name)
68+
69+
5970
@pytest.fixture
6071
def rot13_filter() -> Generator[None, None, None]:
61-
pygit2.filter_register('rot13', _Rot13Filter)
62-
yield
63-
pygit2.filter_unregister('rot13')
72+
yield from _filter_fixture('rot13', _Rot13Filter)
6473

6574

6675
@pytest.fixture
6776
def passthrough_filter() -> Generator[None, None, None]:
68-
pygit2.filter_register('passthrough-rot13', _PassthroughFilter)
69-
yield
70-
pygit2.filter_unregister('passthrough-rot13')
77+
yield from _filter_fixture('passthrough-rot13', _PassthroughFilter)
7178

7279

7380
@pytest.fixture
7481
def buffered_filter() -> Generator[None, None, None]:
75-
pygit2.filter_register('buffered-rot13', _BufferedFilter)
76-
yield
77-
pygit2.filter_unregister('buffered-rot13')
82+
yield from _filter_fixture('buffered-rot13', _BufferedFilter)
7883

7984

8085
@pytest.fixture
8186
def unmatched_filter() -> Generator[None, None, None]:
82-
pygit2.filter_register('unmatched-rot13', _UnmatchedFilter)
83-
yield
84-
pygit2.filter_unregister('unmatched-rot13')
87+
yield from _filter_fixture('unmatched-rot13', _UnmatchedFilter)
8588

8689

8790
def test_filter(testrepo: Repository, rot13_filter: Filter) -> None:
@@ -153,3 +156,29 @@ def test_filterlist_crlf(testrepo: Repository) -> None:
153156

154157
with pytest.raises(TypeError):
155158
1234 in fl # type: ignore
159+
160+
161+
def test_filterlist_rot13(testrepo: Repository, rot13_filter: Filter) -> None:
162+
fl = testrepo.load_filter_list('hello.txt')
163+
assert fl is not None
164+
assert 'rot13' in fl
165+
166+
167+
def test_filterlist_dangerous_unregister(testrepo: Repository) -> None:
168+
pygit2.filter_register('rot13', _Rot13Filter)
169+
170+
fl = testrepo.load_filter_list('hello.txt')
171+
assert fl is not None
172+
assert len(fl) == 1
173+
assert 'rot13' in fl
174+
175+
# Unregistering a filter that's still in use in a FilterList is dangerous!
176+
# Our built-in check (that raises RuntimeError) may avert a segfault.
177+
with pytest.raises(RuntimeError):
178+
pygit2.filter_unregister('rot13')
179+
180+
# Delete any FilterLists that use the filter, and only then is it safe
181+
# to unregister the filter.
182+
del fl
183+
gc.collect()
184+
pygit2.filter_unregister('rot13')

0 commit comments

Comments
 (0)