Skip to content

Commit 1298f00

Browse files
pks-tgitster
authored andcommitted
refs/files: detect race when generating reflog entry for HEAD
When updating a reference that is being pointed to HEAD we don't only write a reflog message for that particular reference, but also generate one for HEAD. This logic is handled by `split_head_update()`, where we: 1. Verify that the condition actually triggered. This is done by reading HEAD at the start of the transaction so that we can then check whether a given reference update refers to its target. 2. Queue a new log-only update for HEAD in case it did. But the logic is unfortunately not free of races, as we do not lock the HEAD reference after we have read its target. This can lead to the following two scenarios: - HEAD gets concurrently updated to point to one of the references we have already processed. This causes us not writing a reflog message even though we should have done so. - HEAD gets concurrently updated to no longer point to a reference anymore that we have already processed. This causes us to write a reflog message even though we should _not_ have done so. Improve the situation by introducing a new `REF_LOG_VIA_SPLIT` flag that is specific to the "files" backend. If set, we will double check that the HEAD reference still points to the reference that we are creating the reflog entry for after we have locked HEAD. Furthermore, instead of manually resolving the old object ID of that entry, we now use the same old state as for the parent update. Unfortunately, this change only helps with the second race. We cannot reliably plug the first race without locking the HEAD reference at the start of the transaction. Locking HEAD unconditionally would effectively serialize all writes though, and that doesn't seem like an option. Also, double checking its value at the end of the transaction is not an option either, as its target may have flip-flopped during the transaction. Signed-off-by: Patrick Steinhardt <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent b928afa commit 1298f00

File tree

1 file changed

+55
-3
lines changed

1 file changed

+55
-3
lines changed

refs/files-backend.c

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@
6868
*/
6969
#define REF_DELETED_RMDIR (1 << 9)
7070

71+
/*
72+
* Used to indicate that the reflog-only update has been created via
73+
* `split_head_update()`.
74+
*/
75+
#define REF_LOG_VIA_SPLIT (1 << 14)
76+
7177
struct ref_lock {
7278
char *ref_name;
7379
struct lock_file lk;
@@ -637,14 +643,16 @@ int parse_loose_ref_contents(const struct git_hash_algo *algop,
637643
return 0;
638644
}
639645

640-
static void unlock_ref(struct ref_lock *lock)
646+
static int unlock_ref(struct ref_lock *lock)
641647
{
642648
lock->count--;
643649
if (!lock->count) {
644650
rollback_lock_file(&lock->lk);
645651
free(lock->ref_name);
646652
free(lock);
653+
return 1;
647654
}
655+
return 0;
648656
}
649657

650658
/*
@@ -2420,9 +2428,10 @@ static enum ref_transaction_error split_head_update(struct ref_update *update,
24202428

24212429
new_update = ref_transaction_add_update(
24222430
transaction, "HEAD",
2423-
update->flags | REF_LOG_ONLY | REF_NO_DEREF,
2431+
update->flags | REF_LOG_ONLY | REF_NO_DEREF | REF_LOG_VIA_SPLIT,
24242432
&update->new_oid, &update->old_oid,
24252433
NULL, NULL, update->committer_info, update->msg);
2434+
new_update->parent_update = update;
24262435

24272436
/*
24282437
* Add "HEAD". This insertion is O(N) in the transaction
@@ -2550,6 +2559,9 @@ struct files_transaction_backend_data {
25502559
* the referent to transaction.
25512560
* - If it is an update of head_ref, add a corresponding REF_LOG_ONLY
25522561
* update of HEAD.
2562+
*
2563+
* Returns 0 on success, 1 in case the update needs to be dropped or a negative
2564+
* error code otherwise.
25532565
*/
25542566
static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *refs,
25552567
struct ref_update *update,
@@ -2600,7 +2612,45 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
26002612

26012613
update->backend_data = lock;
26022614

2603-
if (update->type & REF_ISSYMREF) {
2615+
if (update->flags & REF_LOG_VIA_SPLIT) {
2616+
struct ref_lock *parent_lock;
2617+
2618+
if (!update->parent_update)
2619+
BUG("split update without a parent");
2620+
2621+
parent_lock = update->parent_update->backend_data;
2622+
2623+
/*
2624+
* Check that "HEAD" didn't racily change since we have looked
2625+
* it up. If it did we remove the reflog-only updateg from the
2626+
* transaction again.
2627+
*
2628+
* Note that this does not catch all races: if "HEAD" was
2629+
* racily changed to point to one of the refs part of the
2630+
* transaction then we would miss writing the split reflog
2631+
* entry for "HEAD".
2632+
*/
2633+
if (!(update->type & REF_ISSYMREF) ||
2634+
strcmp(update->parent_update->refname, referent.buf)) {
2635+
if (unlock_ref(lock))
2636+
strmap_remove(&backend_data->ref_locks,
2637+
update->refname, 0);
2638+
2639+
memmove(transaction->updates + update_idx,
2640+
transaction->updates + update_idx + 1,
2641+
(transaction->nr - update_idx - 1) * sizeof(*transaction->updates));
2642+
transaction->nr--;
2643+
2644+
ret = 1;
2645+
goto out;
2646+
}
2647+
2648+
if (update->flags & REF_HAVE_OLD) {
2649+
oidcpy(&lock->old_oid, &update->old_oid);
2650+
} else {
2651+
oidcpy(&lock->old_oid, &parent_lock->old_oid);
2652+
}
2653+
} else if (update->type & REF_ISSYMREF) {
26042654
if (update->flags & REF_NO_DEREF) {
26052655
/*
26062656
* We won't be reading the referent as part of
@@ -2860,6 +2910,8 @@ static int files_transaction_prepare(struct ref_store *ref_store,
28602910
head_ref, &refnames_to_check,
28612911
err);
28622912
if (ret) {
2913+
if (ret > 0)
2914+
continue;
28632915
if (ref_transaction_maybe_set_rejected(transaction, i, ret)) {
28642916
strbuf_reset(err);
28652917
ret = 0;

0 commit comments

Comments
 (0)