object-file: avoid ODB transaction when not writing objects

In ce1661f9da (odb: add transaction interface, 2025-09-16), existing ODB transaction logic is adapted to create a transaction interface at the ODB layer. The intent here is for the ODB transaction interface to eventually provide an object source agnostic means to manage transactions. An unintended consequence of this change though is that `object-file.c:index_fd()` may enter the ODB transaction path even when no object write is requested. In non-repository contexts, this can result in a NULL dereference and segfault. One such case occurs when running git-diff(1) outside of a repository with "core.bigFileThreshold" forcing the streaming path in `index_fd()`: $ echo foo >foo $ echo bar >bar $ git -c core.bigFileThreshold=1 diff -- foo bar In this scenario, the caller only needs to compute the object ID. Object hashing does not require an ODB, so starting a transaction is both unnecessary and invalid. Fix the bug by avoiding the use of ODB transactions in `index_fd()` when callers are only interested in computing the object hash. Reported-by: Luca Stefani <luca.stefani.ge1@gmail.com> Signed-off-by: Justin Tobler <jltobler@gmail.com> [jc: adjusted to fd13909e (Merge branch 'jt/odb-transaction', 2025-10-02)] Signed-off-by: Junio C Hamano <gitster@pobox.com>

Justin Tobler committed Apr 7, 2026 at 15:17 UTC 7d8727ff0b621a9729c2de6a3698063b7b3ba2d6
2 files changed +49 -8
object-file.c
+41 -8
@@ -1599,6 +1599,34 @@ static int index_blob_packfile_transaction(struct odb_transaction *transaction,
1599 return 0;
1600 }
1601
1602 +static int hash_blob_stream(const struct git_hash_algo *hash_algo,
1603 + struct object_id *result_oid, int fd, size_t size)
1604 +{
1605 + unsigned char buf[16384];
1606 + struct git_hash_ctx ctx;
1607 + unsigned header_len;
1608 +
1609 + header_len = format_object_header((char *)buf, sizeof(buf),
1610 + OBJ_BLOB, size);
1611 + hash_algo->init_fn(&ctx);
1612 + git_hash_update(&ctx, buf, header_len);
1613 +
1614 + while (size) {
1615 + size_t rsize = size < sizeof(buf) ? size : sizeof(buf);
1616 + ssize_t read_result = read_in_full(fd, buf, rsize);
1617 +
1618 + if ((read_result < 0) || ((size_t)read_result != rsize))
1619 + return -1;
1620 +
1621 + git_hash_update(&ctx, buf, rsize);
1622 + size -= read_result;
1623 + }
1624 +
1625 + git_hash_final_oid(result_oid, &ctx);
1626 +
1627 + return 0;
1628 +}
1629 +
1630 int index_fd(struct index_state *istate, struct object_id *oid,
1631 int fd, struct stat *st,
1632 enum object_type type, const char *path, unsigned flags)
@@ -1620,14 +1648,19 @@ int index_fd(struct index_state *istate, struct object_id *oid,
1648 ret = index_core(istate, oid, fd, xsize_t(st->st_size),
1649 type, path, flags);
1650 } else {
1623 - struct odb_transaction *transaction;
1624 -
1625 - transaction = odb_transaction_begin(the_repository->objects);
1626 - ret = index_blob_packfile_transaction(the_repository->objects->transaction,
1627 - oid, fd,
1628 - xsize_t(st->st_size),
1629 - path, flags);
1630 - odb_transaction_commit(transaction);
1651 + if (flags & INDEX_WRITE_OBJECT) {
1652 + struct odb_transaction *transaction;
1653 +
1654 + transaction = odb_transaction_begin(the_repository->objects);
1655 + ret = index_blob_packfile_transaction(the_repository->objects->transaction,
1656 + oid, fd,
1657 + xsize_t(st->st_size),
1658 + path, flags);
1659 + odb_transaction_commit(transaction);
1660 + } else {
1661 + ret = hash_blob_stream(the_repository->hash_algo, oid,
1662 + fd, xsize_t(st->st_size));
1663 + }
1664 }
1665
1666 close(fd);
t/t1517-outside-repo.sh
+8
@@ -93,6 +93,14 @@ test_expect_success 'diff outside repository' '
93 test_cmp expect actual
94 '
95
96 +test_expect_success 'hash object exceeding bigFileThreshold outside repository' '
97 + (
98 + cd non-repo &&
99 + echo foo >foo &&
100 + git -c core.bigFileThreshold=1 hash-object --stdin <foo
101 + )
102 +'
103 +
104 test_expect_success 'stripspace outside repository' '
105 nongit git stripspace -s </dev/null
106 '