Skip to content

Commit 93209db

Browse files
committed
odb backend: acquire the GIL in the callbacks
The callbacks libgit2 calls for an OdbBackend implemented in Python called into Python without holding the GIL. That works when libgit2 is entered from the C extension, which keeps the GIL, but not from cffi, which releases it around every C call: Index.add on a repository with such a backend crashes the interpreter (access violation on Windows). Acquire the GIL with PyGILState_Ensure, like the filter callbacks do. Assisted-by: Claude Fable 5.1
1 parent 58d8134 commit 93209db

2 files changed

Lines changed: 186 additions & 74 deletions

File tree

‎src/odb_backend.c‎

Lines changed: 129 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -56,39 +56,50 @@ typedef struct {
5656
PyObject *py_backend;
5757
} pgit_odb_backend;
5858

59+
/*
60+
* The callbacks below may be called by libgit2 without the GIL: cffi releases
61+
* it around every C call, and functions such as git_index_add_bypath end up
62+
* here. So they must acquire it before calling into Python.
63+
*/
64+
5965
static int
6066
pgit_odb_backend_read(void **ptr, size_t *sz, git_object_t *type,
6167
git_odb_backend *_be, const git_oid *oid)
6268
{
6369
pgit_odb_backend *be = (pgit_odb_backend *)_be;
70+
PyObject *result = NULL;
71+
int err = GIT_EUSER;
72+
PyGILState_STATE gil = PyGILState_Ensure();
6473

6574
PyObject *py_oid = git_oid_to_python(oid);
6675
if (py_oid == NULL)
67-
return GIT_EUSER;
76+
goto done;
6877

69-
PyObject *result = PyObject_CallMethod(be->py_backend, "read_cb", "N", py_oid);
70-
if (result == NULL)
71-
return git_error_for_exc();
78+
result = PyObject_CallMethod(be->py_backend, "read_cb", "N", py_oid);
79+
if (result == NULL) {
80+
err = git_error_for_exc();
81+
goto done;
82+
}
7283

7384
const char *bytes;
7485
Py_ssize_t type_value;
7586
Py_ssize_t py_sz;
76-
if (!PyArg_ParseTuple(result, "ny#", &type_value, &bytes, &py_sz) || !bytes) {
77-
Py_DECREF(result);
78-
return GIT_EUSER;
79-
}
87+
if (!PyArg_ParseTuple(result, "ny#", &type_value, &bytes, &py_sz) || !bytes)
88+
goto done;
8089
*type = (git_object_t)type_value;
8190
*sz = (size_t)py_sz;
8291

8392
*ptr = git_odb_backend_data_alloc(_be, *sz);
84-
if (!*ptr) {
85-
Py_DECREF(result);
86-
return GIT_EUSER;
87-
}
93+
if (!*ptr)
94+
goto done;
8895

8996
memcpy(*ptr, bytes, *sz);
90-
Py_DECREF(result);
91-
return 0;
97+
err = 0;
98+
99+
done:
100+
Py_XDECREF(result);
101+
PyGILState_Release(gil);
102+
return err;
92103
}
93104

94105
static int
@@ -101,94 +112,119 @@ pgit_odb_backend_read_prefix(git_oid *oid_out, void **ptr, size_t *sz, git_objec
101112

102113
// Call callback
103114
pgit_odb_backend *be = (pgit_odb_backend *)_be;
115+
int err = GIT_EUSER;
116+
PyGILState_STATE gil = PyGILState_Ensure();
104117
PyObject *result = PyObject_CallMethod(be->py_backend, "read_prefix_cb", "s#", short_id_hex, len);
105-
if (result == NULL)
106-
return git_error_for_exc();
118+
if (result == NULL) {
119+
err = git_error_for_exc();
120+
goto done;
121+
}
107122

108123
// Parse output from callback
109124
PyObject *py_oid_out;
110125
Py_ssize_t type_value;
111126
Py_ssize_t py_sz;
112127
const char *bytes;
113-
if (!PyArg_ParseTuple(result, "ny#O", &type_value, &bytes, &py_sz, &py_oid_out) || !bytes) {
114-
Py_DECREF(result);
115-
return GIT_EUSER;
116-
}
128+
if (!PyArg_ParseTuple(result, "ny#O", &type_value, &bytes, &py_sz, &py_oid_out) || !bytes)
129+
goto done;
117130
*type = (git_object_t)type_value;
118131
*sz = (size_t)py_sz;
119132

120133
*ptr = git_odb_backend_data_alloc(_be, *sz);
121-
if (!*ptr) {
122-
Py_DECREF(result);
123-
return GIT_EUSER;
124-
}
134+
if (!*ptr)
135+
goto done;
125136

126137
memcpy(*ptr, bytes, *sz);
127-
size_t oid_len = py_oid_to_git_oid(py_oid_out, oid_out);
128-
Py_DECREF(result);
129-
if (oid_len == 0)
130-
return GIT_EUSER;
131-
return 0;
138+
if (py_oid_to_git_oid(py_oid_out, oid_out) == 0)
139+
goto done;
140+
err = 0;
141+
142+
done:
143+
Py_XDECREF(result);
144+
PyGILState_Release(gil);
145+
return err;
132146
}
133147

134148
static int
135149
pgit_odb_backend_read_header(size_t *len, git_object_t *type,
136150
git_odb_backend *_be, const git_oid *oid)
137151
{
138152
pgit_odb_backend *be = (pgit_odb_backend *)_be;
153+
PyObject *result = NULL;
154+
int err = GIT_EUSER;
155+
PyGILState_STATE gil = PyGILState_Ensure();
139156

140157
PyObject *py_oid = git_oid_to_python(oid);
141158
if (py_oid == NULL)
142-
return GIT_EUSER;
159+
goto done;
143160

144-
PyObject *result = PyObject_CallMethod(be->py_backend, "read_header_cb", "N", py_oid);
145-
if (result == NULL)
146-
return git_error_for_exc();
161+
result = PyObject_CallMethod(be->py_backend, "read_header_cb", "N", py_oid);
162+
if (result == NULL) {
163+
err = git_error_for_exc();
164+
goto done;
165+
}
147166

148167
Py_ssize_t type_value;
149-
if (!PyArg_ParseTuple(result, "nn", &type_value, len)) {
150-
Py_DECREF(result);
151-
return GIT_EUSER;
152-
}
168+
if (!PyArg_ParseTuple(result, "nn", &type_value, len))
169+
goto done;
153170
*type = (git_object_t)type_value;
171+
err = 0;
154172

155-
Py_DECREF(result);
156-
return 0;
173+
done:
174+
Py_XDECREF(result);
175+
PyGILState_Release(gil);
176+
return err;
157177
}
158178

159179
static int
160180
pgit_odb_backend_write(git_odb_backend *_be, const git_oid *oid,
161181
const void *data, size_t sz, git_object_t typ)
162182
{
163183
pgit_odb_backend *be = (pgit_odb_backend *)_be;
184+
PyObject *result = NULL;
185+
int err = GIT_EUSER;
186+
PyGILState_STATE gil = PyGILState_Ensure();
164187

165188
PyObject *py_oid = git_oid_to_python(oid);
166189
if (py_oid == NULL)
167-
return GIT_EUSER;
190+
goto done;
168191

169-
PyObject *result = PyObject_CallMethod(be->py_backend, "write_cb", "Ny#n", py_oid, data, sz, typ);
170-
if (result == NULL)
171-
return git_error_for_exc();
192+
result = PyObject_CallMethod(be->py_backend, "write_cb", "Ny#n", py_oid, data, sz, typ);
193+
if (result == NULL) {
194+
err = git_error_for_exc();
195+
goto done;
196+
}
197+
err = 0;
172198

173-
Py_DECREF(result);
174-
return 0;
199+
done:
200+
Py_XDECREF(result);
201+
PyGILState_Release(gil);
202+
return err;
175203
}
176204

177205
static int
178206
pgit_odb_backend_exists(git_odb_backend *_be, const git_oid *oid)
179207
{
180208
pgit_odb_backend *be = (pgit_odb_backend *)_be;
209+
PyObject *result = NULL;
210+
int r = GIT_EUSER;
211+
PyGILState_STATE gil = PyGILState_Ensure();
181212

182213
PyObject *py_oid = git_oid_to_python(oid);
183214
if (py_oid == NULL)
184-
return GIT_EUSER;
215+
goto done;
216+
217+
result = PyObject_CallMethod(be->py_backend, "exists_cb", "N", py_oid);
218+
if (result == NULL) {
219+
r = git_error_for_exc();
220+
goto done;
221+
}
185222

186-
PyObject *result = PyObject_CallMethod(be->py_backend, "exists_cb", "N", py_oid);
187-
if (result == NULL)
188-
return git_error_for_exc();
223+
r = PyObject_IsTrue(result);
189224

190-
int r = PyObject_IsTrue(result);
191-
Py_DECREF(result);
225+
done:
226+
Py_XDECREF(result);
227+
PyGILState_Release(gil);
192228
return r;
193229
}
194230

@@ -202,67 +238,86 @@ pgit_odb_backend_exists_prefix(git_oid *out, git_odb_backend *_be,
202238

203239
// Call callback
204240
pgit_odb_backend *be = (pgit_odb_backend *)_be;
241+
int err = GIT_EUSER;
242+
PyGILState_STATE gil = PyGILState_Ensure();
205243
PyObject *py_oid = PyObject_CallMethod(be->py_backend, "exists_prefix_cb", "s#", short_id_hex, len);
206-
if (py_oid == NULL)
207-
return git_error_for_exc();
244+
if (py_oid == NULL) {
245+
err = git_error_for_exc();
246+
goto done;
247+
}
208248

209-
size_t oid_len = py_oid_to_git_oid(py_oid, out);
210-
Py_DECREF(py_oid);
211-
if (oid_len == 0)
212-
return GIT_EUSER;
213-
return 0;
249+
if (py_oid_to_git_oid(py_oid, out) == 0)
250+
goto done;
251+
err = 0;
252+
253+
done:
254+
Py_XDECREF(py_oid);
255+
PyGILState_Release(gil);
256+
return err;
214257
}
215258

216259
static int
217260
pgit_odb_backend_refresh(git_odb_backend *_be)
218261
{
219262
pgit_odb_backend *be = (pgit_odb_backend *)_be;
263+
PyGILState_STATE gil = PyGILState_Ensure();
220264
PyObject_CallMethod(be->py_backend, "refresh_cb", NULL);
221-
return git_error_for_exc();
265+
int err = git_error_for_exc();
266+
PyGILState_Release(gil);
267+
return err;
222268
}
223269

224270
static int
225271
pgit_odb_backend_foreach(git_odb_backend *_be,
226272
git_odb_foreach_cb cb, void *payload)
227273
{
228274
PyObject *item;
275+
PyObject *iterator = NULL;
229276
git_oid oid;
230277
pgit_odb_backend *be = (pgit_odb_backend *)_be;
278+
int err = GIT_EUSER;
279+
PyGILState_STATE gil = PyGILState_Ensure();
231280

232281
/* Call the Python __iter__ method directly. PyObject_GetIter would invoke
233282
* the C tp_iter slot (OdbBackend_as_iter), which calls this function back
234283
* and causes infinite recursion for Python backends. */
235284
PyObject *iter_method = PyObject_GetAttrString((PyObject *)be->py_backend, "__iter__");
236-
if (iter_method == NULL)
237-
return git_error_for_exc();
285+
if (iter_method == NULL) {
286+
err = git_error_for_exc();
287+
goto done;
288+
}
238289

239-
PyObject *iterator = PyObject_CallObject(iter_method, NULL);
290+
iterator = PyObject_CallObject(iter_method, NULL);
240291
Py_DECREF(iter_method);
241-
if (iterator == NULL)
242-
return git_error_for_exc();
292+
if (iterator == NULL) {
293+
err = git_error_for_exc();
294+
goto done;
295+
}
243296

244297
while ((item = PyIter_Next(iterator))) {
245298
size_t len = py_oid_to_git_oid(item, &oid);
246299
Py_DECREF(item);
247-
if (len == 0) {
248-
Py_DECREF(iterator);
249-
return GIT_EUSER;
250-
}
251-
if (cb(&oid, payload) != 0) {
252-
Py_DECREF(iterator);
253-
return GIT_EUSER;
254-
}
300+
if (len == 0)
301+
goto done;
302+
if (cb(&oid, payload) != 0)
303+
goto done;
255304
}
256305

257-
Py_DECREF(iterator);
258-
return git_error_for_exc();
306+
err = git_error_for_exc();
307+
308+
done:
309+
Py_XDECREF(iterator);
310+
PyGILState_Release(gil);
311+
return err;
259312
}
260313

261314
static void
262315
pgit_odb_backend_free(git_odb_backend *backend)
263316
{
264317
pgit_odb_backend *custom_backend = (pgit_odb_backend *)backend;
318+
PyGILState_STATE gil = PyGILState_Ensure();
265319
Py_DECREF(custom_backend->py_backend);
320+
PyGILState_Release(gil);
266321
}
267322

268323
int

‎test/test_odb_backend.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
# Standard Library
2929
import binascii
30+
import subprocess
31+
import sys
3032
from collections.abc import Generator, Iterator
3133
from pathlib import Path
3234

@@ -277,3 +279,58 @@ def test_foreach_cb_bad_oid(barerepo: Repository) -> None:
277279
odb.add_backend(backend, 1)
278280
with pytest.raises(pygit2.InvalidError):
279281
next(iter(odb))
282+
283+
284+
#
285+
# Test a custom object backend called back from a cffi function.
286+
#
287+
288+
INDEX_ADD_SCRIPT = """
289+
import sys
290+
import pygit2
291+
from pygit2.enums import ObjectType
292+
293+
294+
class MemoryBackend(pygit2.OdbBackend):
295+
def __init__(self):
296+
super().__init__()
297+
self.objects = {}
298+
299+
def read_cb(self, oid):
300+
return ObjectType.BLOB, self.objects[oid]
301+
302+
def read_prefix_cb(self, prefix):
303+
raise KeyError(prefix)
304+
305+
def read_header_cb(self, oid):
306+
return ObjectType.BLOB, len(self.objects[oid])
307+
308+
def exists_cb(self, oid):
309+
return oid in self.objects
310+
311+
def exists_prefix_cb(self, prefix):
312+
raise KeyError(prefix)
313+
314+
def refresh_cb(self):
315+
pass
316+
317+
def write_cb(self, oid, data, typ):
318+
self.objects[oid] = data
319+
320+
321+
repo = pygit2.Repository(sys.argv[1])
322+
backend = MemoryBackend()
323+
repo.odb.add_backend(backend, 100)
324+
repo.index.add('hello.txt')
325+
assert backend.objects[repo.index['hello.txt'].id] == b'hello'
326+
"""
327+
328+
329+
def test_index_add(testrepo: Repository) -> None:
330+
# Index.add calls libgit2 through cffi, which releases the GIL: the backend
331+
# callbacks must acquire it. Without the GIL the interpreter crashes, so
332+
# run in a subprocess.
333+
(Path(testrepo.workdir) / 'hello.txt').write_bytes(b'hello')
334+
subprocess.run(
335+
[sys.executable, '-c', INDEX_ADD_SCRIPT, testrepo.workdir], check=True
336+
)

0 commit comments

Comments
 (0)