Skip to content

Commit e6204dc

Browse files
committed
Fix invalid oid handling in BlobIO, Reference, RefdbBackend and ODB backends
Invalid oid arguments were silently ignored or left a Python exception set, which could produce wrong results, SystemError, or crashes. Now: - Blob__write_to_queue, Reference_init, RefdbBackend_write/delete check py_oid_to_git_oid return value and propagate the Python exception. - Custom ODB backend callbacks return GIT_EUSER on invalid oids. - Odb_as_iter and OdbBackend_as_iter propagate GIT_EUSER errors without goto. - OdbBackend_read_prefix/exists_prefix preserve Python exceptions from custom backends. Regression tests added. Fixes #1478 Assisted-by: Kimi Code
1 parent b9f62a3 commit e6204dc

10 files changed

Lines changed: 223 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
[#962](https://github.com/libgit2/pygit2/issues/962)
1212
[#1100](https://github.com/libgit2/pygit2/pull/1100).
1313

14+
- Fix invalid oid arguments being silently ignored in `BlobIO`, the
15+
`Reference` constructor, `RefdbBackend.write()`/`delete()`, and custom
16+
`OdbBackend` callbacks; they now raise the appropriate Python exception
17+
instead of producing wrong results, `SystemError`, or crashes
18+
[#1478](https://github.com/libgit2/pygit2/issues/1478).
19+
1420

1521
# 1.20.0 (2026-08-08)
1622

src/blob.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,11 @@ Blob__write_to_queue(Blob *self, PyObject *args, PyObject *kwds)
316316
{
317317
if (py_oid != NULL && py_oid != Py_None)
318318
{
319-
err = py_oid_to_git_oid(py_oid, &opts.attr_commit_id);
320-
if (err < 0)
321-
return Error_set(err);
319+
size_t len = py_oid_to_git_oid(py_oid, &opts.attr_commit_id);
320+
if (len == 0) {
321+
git_blob_free(blob);
322+
return NULL;
323+
}
322324
}
323325

324326
if ((opts.flags & GIT_BLOB_FILTER_NO_SYSTEM_ATTRIBUTES) != 0)

src/odb.c

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,24 @@ Odb_build_as_iter(const git_oid *oid, void *accum)
113113
PyObject *
114114
Odb_as_iter(Odb *self)
115115
{
116-
int err;
117116
PyObject *accum = PyList_New(0);
118-
PyObject *ret = NULL;
117+
if (accum == NULL)
118+
return NULL;
119119

120-
err = git_odb_foreach(self->odb, Odb_build_as_iter, (void*)accum);
120+
int err = git_odb_foreach(self->odb, Odb_build_as_iter, (void*)accum);
121+
if (err == GIT_EUSER && PyErr_Occurred()) {
122+
Py_DECREF(accum);
123+
return NULL;
124+
}
121125
if (err == GIT_EUSER)
122-
goto exit;
126+
err = GIT_ERROR;
127+
123128
if (err < 0) {
124-
ret = Error_set(err);
125-
goto exit;
129+
Py_DECREF(accum);
130+
return Error_set(err);
126131
}
127132

128-
ret = PyObject_GetIter(accum);
129-
130-
exit:
133+
PyObject *ret = PyObject_GetIter(accum);
131134
Py_DECREF(accum);
132135
return ret;
133136
}

src/odb_backend.c

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ pgit_odb_backend_read_prefix(git_oid *oid_out, void **ptr, size_t *sz, git_objec
124124
}
125125

126126
memcpy(*ptr, bytes, *sz);
127-
py_oid_to_git_oid(py_oid_out, oid_out);
127+
size_t oid_len = py_oid_to_git_oid(py_oid_out, oid_out);
128128
Py_DECREF(result);
129+
if (oid_len == 0)
130+
return GIT_EUSER;
129131
return 0;
130132
}
131133

@@ -204,8 +206,10 @@ pgit_odb_backend_exists_prefix(git_oid *out, git_odb_backend *_be,
204206
if (py_oid == NULL)
205207
return git_error_for_exc();
206208

207-
py_oid_to_git_oid(py_oid, out);
209+
size_t oid_len = py_oid_to_git_oid(py_oid, out);
208210
Py_DECREF(py_oid);
211+
if (oid_len == 0)
212+
return GIT_EUSER;
209213
return 0;
210214
}
211215

@@ -224,15 +228,33 @@ pgit_odb_backend_foreach(git_odb_backend *_be,
224228
PyObject *item;
225229
git_oid oid;
226230
pgit_odb_backend *be = (pgit_odb_backend *)_be;
227-
PyObject *iterator = PyObject_GetIter((PyObject *)be->py_backend);
228-
assert(iterator);
231+
232+
/* Call the Python __iter__ method directly. PyObject_GetIter would invoke
233+
* the C tp_iter slot (OdbBackend_as_iter), which calls this function back
234+
* and causes infinite recursion for Python backends. */
235+
PyObject *iter_method = PyObject_GetAttrString((PyObject *)be->py_backend, "__iter__");
236+
if (iter_method == NULL)
237+
return git_error_for_exc();
238+
239+
PyObject *iterator = PyObject_CallObject(iter_method, NULL);
240+
Py_DECREF(iter_method);
241+
if (iterator == NULL)
242+
return git_error_for_exc();
229243

230244
while ((item = PyIter_Next(iterator))) {
231-
py_oid_to_git_oid(item, &oid);
232-
cb(&oid, payload);
245+
size_t len = py_oid_to_git_oid(item, &oid);
233246
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+
}
234255
}
235256

257+
Py_DECREF(iterator);
236258
return git_error_for_exc();
237259
}
238260

@@ -278,7 +300,7 @@ OdbBackend_init(OdbBackend *self, PyObject *args, PyObject *kwds)
278300
// custom_backend->backend.freshen = pgit_odb_backend_freshen;
279301
// custom_backend->backend.writestream = pgit_odb_backend_writestream;
280302
// custom_backend->backend.readstream = pgit_odb_backend_readstream;
281-
if (PyIter_Check((PyObject *)self))
303+
if (PyObject_HasAttrString((PyObject *)self, "__iter__"))
282304
custom_backend->backend.foreach = pgit_odb_backend_foreach;
283305

284306
// Cross reference (don't incref because it's something internal)
@@ -321,20 +343,23 @@ PyObject *
321343
OdbBackend_as_iter(OdbBackend *self)
322344
{
323345
PyObject *accum = PyList_New(0);
324-
PyObject *iter = NULL;
346+
if (accum == NULL)
347+
return NULL;
325348

326349
int err = self->odb_backend->foreach(self->odb_backend, OdbBackend_build_as_iter, (void*)accum);
350+
if (err == GIT_EUSER && PyErr_Occurred()) {
351+
Py_DECREF(accum);
352+
return NULL;
353+
}
327354
if (err == GIT_EUSER)
328-
goto exit;
355+
err = GIT_ERROR;
329356

330357
if (err < 0) {
331-
Error_set(err);
332-
goto exit;
358+
Py_DECREF(accum);
359+
return Error_set(err);
333360
}
334361

335-
iter = PyObject_GetIter(accum);
336-
337-
exit:
362+
PyObject *iter = PyObject_GetIter(accum);
338363
Py_DECREF(accum);
339364
return iter;
340365
}
@@ -397,8 +422,9 @@ OdbBackend_read_prefix(OdbBackend *self, PyObject *py_hex)
397422

398423
err = self->odb_backend->read_prefix(&oid_out, &data, &size, &type, self->odb_backend, &oid, len);
399424
if (err != 0) {
400-
Error_set_oid(err, &oid, len);
401-
return NULL;
425+
if (err == GIT_EUSER && PyErr_Occurred())
426+
return NULL;
427+
return Error_set_oid(err, &oid, len);
402428
}
403429

404430
PyObject *py_oid_out = git_oid_to_python(&oid_out);
@@ -492,8 +518,11 @@ OdbBackend_exists_prefix(OdbBackend *self, PyObject *py_hex)
492518
git_oid out;
493519
result = self->odb_backend->exists_prefix(&out, self->odb_backend, &oid, len);
494520

495-
if (result < 0)
521+
if (result < 0) {
522+
if (result == GIT_EUSER && PyErr_Occurred())
523+
return NULL;
496524
return Error_set(result);
525+
}
497526

498527
return git_oid_to_python(&out);
499528
}

src/refdb_backend.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,8 @@ RefdbBackend_write(RefdbBackend *self, PyObject *args)
711711
return NULL;
712712

713713
if ((PyObject *)py_old != Py_None) {
714-
py_oid_to_git_oid(py_old, &_old);
714+
if (py_oid_to_git_oid(py_old, &_old) == 0)
715+
return NULL;
715716
old = &_old;
716717
}
717718

@@ -785,7 +786,8 @@ RefdbBackend_delete(RefdbBackend *self, PyObject *args)
785786
return NULL;
786787

787788
if (py_old_id != Py_None) {
788-
py_oid_to_git_oid(py_old_id, &old_id);
789+
if (py_oid_to_git_oid(py_old_id, &old_id) == 0)
790+
return NULL;
789791
err = self->refdb_backend->del(self->refdb_backend,
790792
ref_name, &old_id, old_target);
791793
} else {

src/reference.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,11 @@ Reference_init(Reference *self, PyObject *args, PyObject *kwds)
165165
return -1;
166166
}
167167

168-
py_oid_to_git_oid(py_oid, &oid);
168+
if (py_oid_to_git_oid(py_oid, &oid) == 0)
169+
return -1;
169170
if (py_peel != Py_None) {
170-
py_oid_to_git_oid(py_peel, &peel);
171+
if (py_oid_to_git_oid(py_peel, &peel) == 0)
172+
return -1;
171173
}
172174

173175
self->reference = git_reference__alloc(name, &oid,

test/test_blob.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
import pygit2
3636
from pygit2 import Repository
37-
from pygit2.enums import ObjectType
37+
from pygit2.enums import BlobFilter, ObjectType
3838

3939
from . import utils
4040

@@ -266,3 +266,43 @@ def test_blobio_filtered(testrepo: Repository) -> None:
266266
with pygit2.BlobIO(blob, as_path='bye.txt') as reader:
267267
assert b'bye world\n' == reader.read()
268268
assert not reader.raw._thread.is_alive() # type: ignore[attr-defined]
269+
270+
271+
def test_blob_write_to_queue_invalid_commit_id_type(testrepo: Repository) -> None:
272+
# Regression test (issue #1478): an invalid commit_id type must raise
273+
# TypeError instead of being ignored and leaving an exception set.
274+
queue: Queue[bytes] = Queue()
275+
ready = Event()
276+
done = Event()
277+
blob_oid = testrepo.create_blob_fromworkdir('bye.txt')
278+
blob = testrepo[blob_oid]
279+
assert isinstance(blob, pygit2.Blob)
280+
with pytest.raises(TypeError):
281+
blob._write_to_queue(
282+
queue,
283+
ready,
284+
done,
285+
as_path='bye.txt',
286+
flags=BlobFilter.ATTRIBUTES_FROM_COMMIT,
287+
commit_id=1234, # type: ignore
288+
)
289+
290+
291+
def test_blob_write_to_queue_invalid_commit_id_str(testrepo: Repository) -> None:
292+
# Regression test (issue #1478): a malformed commit_id string must raise
293+
# InvalidError instead of being ignored and leaving an exception set.
294+
queue: Queue[bytes] = Queue()
295+
ready = Event()
296+
done = Event()
297+
blob_oid = testrepo.create_blob_fromworkdir('bye.txt')
298+
blob = testrepo[blob_oid]
299+
assert isinstance(blob, pygit2.Blob)
300+
with pytest.raises(pygit2.InvalidError):
301+
blob._write_to_queue(
302+
queue,
303+
ready,
304+
done,
305+
as_path='bye.txt',
306+
flags=BlobFilter.ATTRIBUTES_FROM_COMMIT,
307+
commit_id='not-a-valid-oid', # type: ignore[arg-type]
308+
)

test/test_odb_backend.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,50 @@ def test_repo_read(repo: Repository) -> None:
169169
ab = repo[BLOB_OID]
170170
a = repo[BLOB_HEX]
171171
assert ab == a
172+
173+
174+
class BadOidReadPrefixBackend(ProxyBackend):
175+
def read_prefix_cb(self, oid: Oid | str) -> tuple[int, bytes, Oid | str]: # type: ignore[override]
176+
return (ObjectType.BLOB, b'bad', 'not-a-valid-oid')
177+
178+
179+
class BadOidExistsPrefixBackend(ProxyBackend):
180+
def exists_prefix_cb(self, oid: Oid | str) -> Oid | str: # type: ignore[override]
181+
return 'not-a-valid-oid'
182+
183+
184+
class BadOidIterBackend(ProxyBackend):
185+
def __iter__(self) -> Iterator[Oid | str]: # type: ignore[override]
186+
yield 'not-a-valid-oid'
187+
188+
189+
def test_read_prefix_cb_bad_oid(barerepo: Repository) -> None:
190+
# Regression test (issue #1478): an ODB backend returning an invalid oid
191+
# from read_prefix_cb must raise InvalidError instead of silently returning
192+
# garbage data.
193+
path = Path(barerepo.path) / 'objects'
194+
backend = BadOidReadPrefixBackend(pygit2.OdbBackendPack(path))
195+
with pytest.raises(pygit2.InvalidError):
196+
backend.read_prefix(BLOB_HEX[:4])
197+
198+
199+
def test_exists_prefix_cb_bad_oid(barerepo: Repository) -> None:
200+
# Regression test (issue #1478): an ODB backend returning an invalid oid
201+
# from exists_prefix_cb must raise InvalidError instead of silently returning
202+
# garbage data.
203+
path = Path(barerepo.path) / 'objects'
204+
backend = BadOidExistsPrefixBackend(pygit2.OdbBackendPack(path))
205+
with pytest.raises(pygit2.InvalidError):
206+
backend.exists_prefix(BLOB_HEX[:4])
207+
208+
209+
def test_foreach_cb_bad_oid(barerepo: Repository) -> None:
210+
# Regression test (issue #1478): an ODB backend yielding an invalid oid
211+
# during iteration must raise InvalidError instead of crashing or returning
212+
# garbage data.
213+
path = Path(barerepo.path) / 'objects'
214+
backend = BadOidIterBackend(pygit2.OdbBackendPack(path))
215+
odb = pygit2.Odb()
216+
odb.add_backend(backend, 1)
217+
with pytest.raises(pygit2.InvalidError):
218+
next(iter(odb))

test/test_refdb_backend.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,42 @@ def test_write(repo: Repository) -> None:
224224
assert repo.backend.lookup('refs/heads/test-write').target == master.target
225225

226226

227+
def test_write_invalid_old_type(repo: Repository) -> None:
228+
# Regression test (issue #1478): RefdbBackend.write must raise TypeError
229+
# when old is not a valid oid, not silently ignore the bad argument.
230+
master = repo.backend.lookup('refs/heads/master')
231+
commit = repo[master.target]
232+
ref = pygit2.Reference('refs/heads/test-write', master.target, None)
233+
with pytest.raises(TypeError):
234+
repo.backend.write(ref, False, commit.author, 'Create test-write', 1234, None) # type: ignore
235+
236+
237+
def test_write_invalid_old_str(repo: Repository) -> None:
238+
# Regression test (issue #1478): RefdbBackend.write must raise InvalidError
239+
# when old is a malformed oid string, not silently ignore the bad argument.
240+
master = repo.backend.lookup('refs/heads/master')
241+
commit = repo[master.target]
242+
ref = pygit2.Reference('refs/heads/test-write', master.target, None)
243+
with pytest.raises(pygit2.InvalidError):
244+
repo.backend.write(
245+
ref, False, commit.author, 'Create test-write', 'not-a-valid-oid', None
246+
)
247+
248+
249+
def test_delete_invalid_old_type(repo: Repository) -> None:
250+
# Regression test (issue #1478): RefdbBackend.delete must raise TypeError
251+
# when old_id is not a valid oid, not silently ignore the bad argument.
252+
with pytest.raises(TypeError):
253+
repo.backend.delete('refs/heads/master', 1234, None) # type: ignore
254+
255+
256+
def test_delete_invalid_old_str(repo: Repository) -> None:
257+
# Regression test (issue #1478): RefdbBackend.delete must raise InvalidError
258+
# when old_id is a malformed oid string, not silently ignore the bad argument.
259+
with pytest.raises(pygit2.InvalidError):
260+
repo.backend.delete('refs/heads/master', 'not-a-valid-oid', None)
261+
262+
227263
def test_rename(repo: Repository) -> None:
228264
old_ref = repo.backend.lookup('refs/heads/i18n')
229265
target = repo.get(old_ref.target)

0 commit comments

Comments
 (0)