refs: don't store peeled object IDs for invalid tags

Both the "files" and "reftable" backend store peeled object IDs for references that point to tags: - The "files" backend stores the value when packing refs, where each peeled object ID is prefixed with "^". - The "reftable" backend stores the value whenever writing a new reference that points to a tag via a special ref record type. Both of these backends use `peel_object()` to find the peeled object ID. But as explained in the preceding commit, that function does not detect the case where the tag's tagged object and its claimed type mismatch. The consequence of storing these bogus peeled object IDs is that we're less likely to detect such corruption in other parts of Git. git-for-each-ref(1) for example does not notice anymore that the tag is broken when using "--format=%(*objectname)" to dereference tags. One could claim that this is good, because it still allows us to mostly use the tag as intended. But the biggest problem here is that we now have different behaviour for such a broken tag depending on whether or not we have its peeled value in the refdb. Fix the issue by verifying the object type when peeling the object. If that verification fails we simply skip storing the peeled value in either of the reference formats. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Oct 23, 2025 at 09:16 UTC 6ec4c0b45ba916770c6fbc03df94018ca06fb77e
4 files changed +63 -2
refs/packed-backend.c
+1 -1
@@ -1528,7 +1528,7 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
1528 } else {
1529 struct object_id peeled;
1530 int peel_error = peel_object(refs->base.repo, &update->new_oid,
1531 - &peeled, 0);
1531 + &peeled, PEEL_OBJECT_VERIFY_OBJECT_TYPE);
1532
1533 if (write_packed_entry(out, update->refname,
1534 &update->new_oid,
refs/reftable-backend.c
+2 -1
@@ -1632,7 +1632,8 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data
1632 ref.refname = (char *)u->refname;
1633 ref.update_index = ts;
1634
1635 - peel_error = peel_object(arg->refs->base.repo, &u->new_oid, &peeled, 0);
1635 + peel_error = peel_object(arg->refs->base.repo, &u->new_oid, &peeled,
1636 + PEEL_OBJECT_VERIFY_OBJECT_TYPE);
1637 if (!peel_error) {
1638 ref.value_type = REFTABLE_REF_VAL2;
1639 memcpy(ref.value.val2.target_value, peeled.hash, GIT_MAX_RAWSZ);
t/pack-refs-tests.sh
+32
@@ -428,4 +428,36 @@ do
428 '
429 done
430
431 +test_expect_success 'pack-refs does not store invalid peeled tag value' '
432 + test_when_finished rm -rf repo &&
433 + git init repo &&
434 + (
435 + cd repo &&
436 + git commit --allow-empty --message initial &&
437 +
438 + echo garbage >blob-content &&
439 + blob_id=$(git hash-object -w -t blob blob-content) &&
440 +
441 + # Write an invalid tag into the object database. The tag itself
442 + # is well-formed, but the tagged object is a blob while we
443 + # claim that it is a commit.
444 + cat >tag-content <<-EOF &&
445 + object $blob_id
446 + type commit
447 + tag bad-tag
448 + tagger C O Mitter <committer@example.com> 1112354055 +0200
449 +
450 + annotated
451 + EOF
452 + tag_id=$(git hash-object -w -t tag tag-content) &&
453 + git update-ref refs/tags/bad-tag "$tag_id" &&
454 +
455 + # The packed-refs file should not contain the peeled object ID.
456 + # If it did this would cause commands that use the peeled value
457 + # to not notice this corrupted tag.
458 + git pack-refs --all &&
459 + test_grep ! "^\^" .git/packed-refs
460 + )
461 +'
462 +
463 test_done
t/t0610-reftable-basics.sh
+28
@@ -1135,4 +1135,32 @@ test_expect_success 'fetch: accessing FETCH_HEAD special ref works' '
1135 test_cmp expect actual
1136 '
1137
1138 +test_expect_success 'writes do not persist peeled value for invalid tags' '
1139 + test_when_finished rm -rf repo &&
1140 + git init repo &&
1141 + (
1142 + cd repo &&
1143 + git commit --allow-empty --message initial &&
1144 +
1145 + # We cannot easily verify that the peeled value is not stored
1146 + # in the tables. Instead, we test this indirectly: we create
1147 + # two tags that both point to the same object, but they claim
1148 + # different object types. If we parse both tags we notice that
1149 + # the parsed tagged object has a mismatch between the two tags
1150 + # and bail out.
1151 + #
1152 + # If we instead use the persisted peeled value we would not
1153 + # even parse the tags. As such, we would not notice the
1154 + # discrepancy either and thus listing these tags would succeed.
1155 + git tag tag-1 -m "tag 1" &&
1156 + git cat-file tag tag-1 >raw-tag &&
1157 + sed "s/^type commit$/type blob/" <raw-tag >broken-tag &&
1158 + broken_tag_id=$(git hash-object -w -t tag broken-tag) &&
1159 + git update-ref refs/tags/tag-2 $broken_tag_id &&
1160 +
1161 + test_must_fail git for-each-ref --format="%(*objectname)" refs/tags/ 2>err &&
1162 + test_grep "bad tag pointer" err
1163 + )
1164 +'
1165 +
1166 test_done