Skip to content

Commit 2d9aec9

Browse files
committed
refdb: make ref deletion after pack safer
In order not to undo concurrent modifications to references, we must make sure that we only delete a loose reference if it still has the same value as when we packed it. This means we need to lock it and then compare the value with the one we put in the packed file.
1 parent 9914efe commit 2d9aec9

File tree

1 file changed

+44
-7
lines changed

1 file changed

+44
-7
lines changed

src/refdb_fs.c

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -901,30 +901,68 @@ static int packed_write_ref(struct packref *ref, git_filebuf *file)
901901
static int packed_remove_loose(refdb_fs_backend *backend)
902902
{
903903
size_t i;
904-
git_buf full_path = GIT_BUF_INIT;
905-
int failed = 0;
904+
git_buf ref_content = GIT_BUF_INIT;
905+
int failed = 0, error = 0;
906906

907907
/* backend->refcache is already locked when this is called */
908908

909909
for (i = 0; i < git_sortedcache_entrycount(backend->refcache); ++i) {
910910
struct packref *ref = git_sortedcache_entry(backend->refcache, i);
911+
git_filebuf lock = GIT_FILEBUF_INIT;
912+
git_oid current_id;
911913

912914
if (!ref || !(ref->flags & PACKREF_WAS_LOOSE))
913915
continue;
914916

915-
if (git_buf_joinpath(&full_path, backend->path, ref->name) < 0)
916-
return -1; /* critical; do not try to recover on oom */
917+
/* We need to stop anybody from updating the ref while we try to do a safe delete */
918+
error = loose_lock(&lock, backend, ref->name);
919+
/* If someone else is updating it, let them do it */
920+
if (error == GIT_EEXISTS)
921+
continue;
922+
923+
if (error < 0) {
924+
failed = 1;
925+
continue;
926+
}
927+
928+
error = git_futils_readbuffer(&ref_content, lock.path_original);
929+
/* Someone else beat us to cleaning up the ref, let's simply continue */
930+
if (error == GIT_ENOTFOUND) {
931+
git_filebuf_cleanup(&lock);
932+
continue;
933+
}
934+
935+
/* This became a symref between us packing and trying to delete it, so ignore it */
936+
if (!git__prefixcmp(ref_content.ptr, GIT_SYMREF)) {
937+
git_filebuf_cleanup(&lock);
938+
continue;
939+
}
940+
941+
/* Figure out the current id; if we fail record it but don't fail the whole operation */
942+
if ((error = loose_parse_oid(&current_id, lock.path_original, &ref_content)) < 0) {
943+
failed = 1;
944+
git_filebuf_cleanup(&lock);
945+
continue;
946+
}
917947

918-
if (git_path_exists(full_path.ptr) && p_unlink(full_path.ptr) < 0) {
948+
/* If the ref moved since we packed it, we must not delete it */
949+
if (!git_oid_equal(&current_id, &ref->oid)) {
950+
git_filebuf_cleanup(&lock);
951+
continue;
952+
}
953+
954+
if (p_unlink(lock.path_original) < 0) {
919955
if (failed)
920956
continue;
921957

922958
giterr_set(GITERR_REFERENCE,
923959
"Failed to remove loose reference '%s' after packing: %s",
924-
full_path.ptr, strerror(errno));
960+
lock.path_original, strerror(errno));
925961
failed = 1;
926962
}
927963

964+
git_filebuf_cleanup(&lock);
965+
928966
/*
929967
* if we fail to remove a single file, this is *not* good,
930968
* but we should keep going and remove as many as possible.
@@ -933,7 +971,6 @@ static int packed_remove_loose(refdb_fs_backend *backend)
933971
*/
934972
}
935973

936-
git_buf_free(&full_path);
937974
return failed ? -1 : 0;
938975
}
939976

0 commit comments

Comments
 (0)