Skip to content

Commit c57e714

Browse files
committed
Avert segfault if attempting to unregister a filter referenced by a FilterList
1 parent 8c89bb9 commit c57e714

File tree

6 files changed

+60
-29
lines changed

6 files changed

+60
-29
lines changed

pygit2/__init__.py

Lines changed: 18 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,24 @@ 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+
from .filter import FilterList
556+
557+
if FilterList._is_filter_in_use(name):
558+
raise RuntimeError(f"filter still in use: '{name}'")
559+
560+
c_name = to_bytes(name)
561+
err = C.git_filter_unregister(c_name)
562+
check_error(err)
563+
564+
548565
tree_entry_key = functools.cmp_to_key(tree_entry_cmp)
549566

550567
settings = Settings()
@@ -610,7 +627,6 @@ def clone_repository(
610627
# Low Level API (not present in .pyi)
611628
'FilterSource',
612629
'filter_register',
613-
'filter_unregister',
614630
'GIT_APPLY_LOCATION_BOTH',
615631
'GIT_APPLY_LOCATION_INDEX',
616632
'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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
2424
# Boston, MA 02110-1301, USA.
2525

26+
import weakref
2627
from collections.abc import Callable
2728

2829
from ._pygit2 import FilterSource, Repository
@@ -113,14 +114,28 @@ def close(self, write_next: Callable[[bytes], None]) -> None:
113114

114115

115116
class FilterList:
117+
_all_filter_lists = set()
118+
116119
@classmethod
117120
def _from_c(cls, ptr):
118121
if ptr == ffi.NULL:
119122
return None
123+
120124
fl = cls.__new__(cls)
121125
fl._pointer = ptr
126+
127+
# Keep track of this FilterList until it's garbage collected. This lets
128+
# `filter_unregister` ensure the user isn't trying to delete a filter
129+
# that's still in use.
130+
ref = weakref.ref(fl, FilterList._all_filter_lists.remove)
131+
FilterList._all_filter_lists.add(ref)
132+
122133
return fl
123134

135+
@classmethod
136+
def _is_filter_in_use(cls, name: str) -> bool:
137+
return any(name in ref() for ref in cls._all_filter_lists)
138+
124139
def __contains__(self, name: str) -> bool:
125140
if not isinstance(name, str):
126141
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,27 @@ def test_filterlist_crlf(testrepo: Repository) -> None:
153153

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

0 commit comments

Comments
 (0)