Skip to content

Commit cc48ea6

Browse files
committed
Fix ownership and error handling bugs in refdb backend callbacks
pygit2_refdb_backend_write() wrapped libgit2's reference and signature pointers directly, so the Python objects freed memory that libgit2 frees again on return, a double free. Pass git_reference_dup() and git_signature_dup() copies instead. It also fed the _old OID pointer straight into git_oid_to_python(), crashing when libgit2 passes NULL for newly created references; map NULL to None, which RefdbBackend.write() already accepts. pygit2_refdb_backend_lookup() treated a None result as a type error, but None is how RefdbBackend.lookup() signals 'not found'; translate it to GIT_ENOTFOUND. Both lookup() and rename() leaked the Python Reference returned by the callback, because libgit2 takes ownership of the underlying git_reference while Reference_dealloc() would free it too. Detach the pointer from the Python object before releasing it. Add test_write_callback and test_write_callback_create, driving the write callback through libgit2's git_reference_set_target() and git_reference_create(). Follow-up to the fix for #1474 Assisted-by: Kimi Code
1 parent 997c65b commit cc48ea6

2 files changed

Lines changed: 77 additions & 16 deletions

File tree

src/refdb_backend.c

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -190,18 +190,30 @@ pygit2_refdb_backend_lookup(git_reference **out,
190190
result = (Reference *)PyObject_CallObject(be->lookup, args);
191191
Py_DECREF(args);
192192

193-
if ((err = git_error_for_exc()) != 0)
194-
goto out;
193+
if ((err = git_error_for_exc()) != 0) {
194+
Py_XDECREF(result);
195+
return err;
196+
}
197+
198+
// A lookup that finds nothing returns None, per RefdbBackend.lookup()
199+
if ((PyObject *)result == Py_None) {
200+
Py_DECREF(result);
201+
return GIT_ENOTFOUND;
202+
}
195203

196204
if (!PyObject_IsInstance((PyObject *)result, (PyObject *)&ReferenceType)) {
197205
PyErr_SetString(PyExc_TypeError, "Expected object of type pygit2.Reference");
198-
err = GIT_EUSER;
199-
goto out;
206+
Py_DECREF(result);
207+
return GIT_EUSER;
200208
}
201209

210+
// Ownership of the underlying git_reference is transferred to libgit2,
211+
// which sets its db field itself once the callback returns; detach it
212+
// from the Python object before releasing the object.
202213
*out = result->reference;
203-
out:
204-
return err;
214+
result->reference = NULL;
215+
Py_DECREF(result);
216+
return 0;
205217
}
206218

207219
static int
@@ -212,24 +224,46 @@ pygit2_refdb_backend_write(git_refdb_backend *_be,
212224
{
213225
struct pygit2_refdb_backend *be = (struct pygit2_refdb_backend *)_be;
214226

215-
PyObject *ref = wrap_reference((git_reference *)_ref, NULL); // XXX: Drops const
227+
// The Python objects take ownership of the reference and the signature,
228+
// so pass them copies; _ref and _who belong to the caller (libgit2).
229+
git_reference *reference;
230+
int err = git_reference_dup(&reference, (git_reference *)_ref); // XXX: Drops const
231+
if (err != 0) {
232+
return err;
233+
}
234+
235+
PyObject *ref = wrap_reference(reference, NULL);
216236
if (ref == NULL) {
237+
git_reference_free(reference);
217238
return GIT_EUSER;
218239
}
219240

220-
PyObject *who = build_signature(NULL, _who, "utf-8");
221-
if (who == NULL) {
241+
git_signature *signature;
242+
err = git_signature_dup(&signature, _who);
243+
if (err != 0) {
222244
Py_DECREF(ref);
223-
return GIT_EUSER;
245+
return err;
224246
}
225247

226-
PyObject *old = git_oid_to_python(_old);
227-
if (old == NULL) {
248+
PyObject *who = build_signature(NULL, signature, "utf-8");
249+
if (who == NULL) {
228250
Py_DECREF(ref);
229-
Py_DECREF(who);
230251
return GIT_EUSER;
231252
}
232253

254+
PyObject *old;
255+
if (_old == NULL) {
256+
old = Py_None;
257+
Py_INCREF(old);
258+
} else {
259+
old = git_oid_to_python(_old);
260+
if (old == NULL) {
261+
Py_DECREF(ref);
262+
Py_DECREF(who);
263+
return GIT_EUSER;
264+
}
265+
}
266+
233267
// Py_BuildValue takes ownership of ref, who and old (N format), even on failure, so
234268
// they must not be decref'd past this point.
235269
PyObject *args = Py_BuildValue("(NNNsNs)", ref, PyBool_FromLong(force), who, message,
@@ -239,7 +273,7 @@ pygit2_refdb_backend_write(git_refdb_backend *_be,
239273
}
240274

241275
PyObject_CallObject(be->write, args);
242-
int err = git_error_for_exc();
276+
err = git_error_for_exc();
243277
Py_DECREF(args);
244278
return err;
245279
}
@@ -277,6 +311,7 @@ pygit2_refdb_backend_rename(git_reference **out, git_refdb_backend *_be,
277311

278312
err = git_error_for_exc();
279313
if (err != 0) {
314+
Py_XDECREF(ref);
280315
return err;
281316
}
282317

@@ -287,9 +322,11 @@ pygit2_refdb_backend_rename(git_reference **out, git_refdb_backend *_be,
287322
}
288323

289324
// Ownership of the underlying git_reference is transferred to libgit2,
290-
// which sets its db field itself once the callback returns, so the Python
291-
// object must not be decref'd; same as in pygit2_refdb_backend_lookup.
325+
// which sets its db field itself once the callback returns; detach it
326+
// from the Python object before releasing the object.
292327
*out = ref->reference;
328+
ref->reference = NULL;
329+
Py_DECREF(ref);
293330
return 0;
294331
}
295332

test/test_refdb_backend.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,30 @@ def test_rename_callback(repo: Repository) -> None:
136136
assert repo.references['refs/heads/intl'].target == target
137137

138138

139+
def test_write_callback(repo: Repository) -> None:
140+
# Exercise the custom backend's write callback through libgit2's
141+
# git_reference_set_target; calling repo.backend.write() directly
142+
# bypasses it.
143+
refdb = pygit2.Refdb.new(repo)
144+
refdb.set_backend(repo.backend)
145+
repo.set_refdb(refdb)
146+
master = repo.references['refs/heads/master']
147+
i18n = repo.references['refs/heads/i18n']
148+
i18n.set_target(master.target)
149+
assert repo.references['refs/heads/i18n'].target == master.target
150+
151+
152+
def test_write_callback_create(repo: Repository) -> None:
153+
# Exercise the custom backend's write callback through libgit2's
154+
# git_reference_create, which passes old=NULL for new references.
155+
refdb = pygit2.Refdb.new(repo)
156+
refdb.set_backend(repo.backend)
157+
repo.set_refdb(refdb)
158+
master = repo.references['refs/heads/master']
159+
repo.references.create('refs/heads/test-write', master.target)
160+
assert repo.references['refs/heads/test-write'].target == master.target
161+
162+
139163
def test_delete(repo: Repository) -> None:
140164
old = repo.backend.lookup('refs/heads/i18n')
141165
repo.backend.delete('refs/heads/i18n', old.target, None)

0 commit comments

Comments
 (0)