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>
This commit is contained in:
Patrick Steinhardt
2025-10-23 09:16:21 +02:00
committed by Junio C Hamano
parent 7ec85185b1
commit 6ec4c0b45b
4 changed files with 63 additions and 2 deletions

View File

@@ -1528,7 +1528,7 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
} else {
struct object_id peeled;
int peel_error = peel_object(refs->base.repo, &update->new_oid,
&peeled, 0);
&peeled, PEEL_OBJECT_VERIFY_OBJECT_TYPE);
if (write_packed_entry(out, update->refname,
&update->new_oid,

View File

@@ -1632,7 +1632,8 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data
ref.refname = (char *)u->refname;
ref.update_index = ts;
peel_error = peel_object(arg->refs->base.repo, &u->new_oid, &peeled, 0);
peel_error = peel_object(arg->refs->base.repo, &u->new_oid, &peeled,
PEEL_OBJECT_VERIFY_OBJECT_TYPE);
if (!peel_error) {
ref.value_type = REFTABLE_REF_VAL2;
memcpy(ref.value.val2.target_value, peeled.hash, GIT_MAX_RAWSZ);

View File

@@ -428,4 +428,36 @@ do
'
done
test_expect_success 'pack-refs does not store invalid peeled tag value' '
test_when_finished rm -rf repo &&
git init repo &&
(
cd repo &&
git commit --allow-empty --message initial &&
echo garbage >blob-content &&
blob_id=$(git hash-object -w -t blob blob-content) &&
# Write an invalid tag into the object database. The tag itself
# is well-formed, but the tagged object is a blob while we
# claim that it is a commit.
cat >tag-content <<-EOF &&
object $blob_id
type commit
tag bad-tag
tagger C O Mitter <committer@example.com> 1112354055 +0200
annotated
EOF
tag_id=$(git hash-object -w -t tag tag-content) &&
git update-ref refs/tags/bad-tag "$tag_id" &&
# The packed-refs file should not contain the peeled object ID.
# If it did this would cause commands that use the peeled value
# to not notice this corrupted tag.
git pack-refs --all &&
test_grep ! "^\^" .git/packed-refs
)
'
test_done

View File

@@ -1135,4 +1135,32 @@ test_expect_success 'fetch: accessing FETCH_HEAD special ref works' '
test_cmp expect actual
'
test_expect_success 'writes do not persist peeled value for invalid tags' '
test_when_finished rm -rf repo &&
git init repo &&
(
cd repo &&
git commit --allow-empty --message initial &&
# We cannot easily verify that the peeled value is not stored
# in the tables. Instead, we test this indirectly: we create
# two tags that both point to the same object, but they claim
# different object types. If we parse both tags we notice that
# the parsed tagged object has a mismatch between the two tags
# and bail out.
#
# If we instead use the persisted peeled value we would not
# even parse the tags. As such, we would not notice the
# discrepancy either and thus listing these tags would succeed.
git tag tag-1 -m "tag 1" &&
git cat-file tag tag-1 >raw-tag &&
sed "s/^type commit$/type blob/" <raw-tag >broken-tag &&
broken_tag_id=$(git hash-object -w -t tag broken-tag) &&
git update-ref refs/tags/tag-2 $broken_tag_id &&
test_must_fail git for-each-ref --format="%(*objectname)" refs/tags/ 2>err &&
test_grep "bad tag pointer" err
)
'
test_done