Skip to content

Commit 85f5da5

Browse files
avargitster
authored andcommitted
hooks: fix a TOCTOU in "did we run a hook?" heuristic
Fix a Time-of-check to time-of-use (TOCTOU) race in code added in 680ee55 (commit: skip discarding the index if there is no pre-commit hook, 2017-08-14). We can fix the race passing around information about whether or not we ran the hook in question, instead of running hook_exists() after the fact to check if the hook in question exists. This problem has been noted on-list when 680ee55 was discussed[1], but had not been fixed. In addition to fixing this for the pre-commit hook as suggested there I'm also fixing this for the pre-merge-commit hook. See 6098817 (git-merge: honor pre-merge-commit hook, 2019-08-07) for the introduction of its previous behavior. Let's also change this for the push-to-checkout hook. Now instead of checking if the hook exists and either doing a push to checkout or a push to deploy we'll always attempt a push to checkout. If the hook doesn't exist we'll fall back on push to deploy. The same behavior as before, without the TOCTOU race. See 0855331 (receive-pack: support push-to-checkout hook, 2014-12-01) for the introduction of the previous behavior. This leaves uses of hook_exists() in two places that matter. The "reference-transaction" check in refs.c, see 6754159 (refs: implement reference transaction hook, 2020-06-19), and the prepare-commit-msg hook, see 66618a5 (sequencer: run 'prepare-commit-msg' hook, 2018-01-24). In both of those cases we're saving ourselves CPU time by not preparing data for the hook that we'll then do nothing with if we don't have the hook. So using this "invoked_hook" pattern doesn't make sense in those cases. More importantly, in those cases the worst we'll do is miss that we "should" run the hook because a new hook appeared, whereas in the pre-commit and pre-merge-commit cases we'll skip an important discard_cache() on the bases of our faulty guess. I do think none of these races really matter in practice. It would be some one-off issue as a hook was added or removed. I did think it was stupid that we didn't pass a "did this run?" flag instead of doing this guessing at a distance though, so now we're not guessing anymore. 1. https://lore.kernel.org/git/[email protected]/ Signed-off-by: Ævar Arnfjörð Bjarmason <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 2faf37f commit 85f5da5

File tree

8 files changed

+45
-19
lines changed

8 files changed

+45
-19
lines changed

builtin/commit.c

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -725,11 +725,13 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
725725
int clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);
726726
int old_display_comment_prefix;
727727
int merge_contains_scissors = 0;
728+
int invoked_hook = 0;
728729

729730
/* This checks and barfs if author is badly specified */
730731
determine_author_info(author_ident);
731732

732-
if (!no_verify && run_commit_hook(use_editor, index_file, "pre-commit", NULL))
733+
if (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook,
734+
"pre-commit", NULL))
733735
return 0;
734736

735737
if (squash_message) {
@@ -1045,10 +1047,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
10451047
return 0;
10461048
}
10471049

1048-
if (!no_verify && hook_exists("pre-commit")) {
1050+
if (!no_verify && invoked_hook) {
10491051
/*
1050-
* Re-read the index as pre-commit hook could have updated it,
1051-
* and write it out as a tree. We must do this before we invoke
1052+
* Re-read the index as the pre-commit-commit hook was invoked
1053+
* and could have updated it. We must do this before we invoke
10521054
* the editor and after we invoke run_status above.
10531055
*/
10541056
discard_cache();
@@ -1060,7 +1062,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
10601062
return 0;
10611063
}
10621064

1063-
if (run_commit_hook(use_editor, index_file, "prepare-commit-msg",
1065+
if (run_commit_hook(use_editor, index_file, NULL, "prepare-commit-msg",
10641066
git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))
10651067
return 0;
10661068

@@ -1077,7 +1079,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
10771079
}
10781080

10791081
if (!no_verify &&
1080-
run_commit_hook(use_editor, index_file, "commit-msg", git_path_commit_editmsg(), NULL)) {
1082+
run_commit_hook(use_editor, index_file, NULL, "commit-msg",
1083+
git_path_commit_editmsg(), NULL)) {
10811084
return 0;
10821085
}
10831086

@@ -1830,7 +1833,8 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
18301833

18311834
repo_rerere(the_repository, 0);
18321835
run_auto_maintenance(quiet);
1833-
run_commit_hook(use_editor, get_index_file(), "post-commit", NULL);
1836+
run_commit_hook(use_editor, get_index_file(), NULL, "post-commit",
1837+
NULL);
18341838
if (amend && !no_post_rewrite) {
18351839
commit_post_rewrite(the_repository, current_head, &oid);
18361840
}

builtin/merge.c

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -844,15 +844,18 @@ static void prepare_to_commit(struct commit_list *remoteheads)
844844
{
845845
struct strbuf msg = STRBUF_INIT;
846846
const char *index_file = get_index_file();
847+
int invoked_hook = 0;
847848

848-
if (!no_verify && run_commit_hook(0 < option_edit, index_file, "pre-merge-commit", NULL))
849+
if (!no_verify && run_commit_hook(0 < option_edit, index_file,
850+
&invoked_hook, "pre-merge-commit",
851+
NULL))
849852
abort_commit(remoteheads, NULL);
850853
/*
851-
* Re-read the index as pre-merge-commit hook could have updated it,
852-
* and write it out as a tree. We must do this before we invoke
854+
* Re-read the index as the pre-merge-commit hook was invoked
855+
* and could have updated it. We must do this before we invoke
853856
* the editor and after we invoke run_status above.
854857
*/
855-
if (hook_exists("pre-merge-commit"))
858+
if (invoked_hook)
856859
discard_cache();
857860
read_cache_from(index_file);
858861
strbuf_addbuf(&msg, &merge_msg);
@@ -873,7 +876,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)
873876
append_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);
874877
write_merge_heads(remoteheads);
875878
write_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);
876-
if (run_commit_hook(0 < option_edit, get_index_file(), "prepare-commit-msg",
879+
if (run_commit_hook(0 < option_edit, get_index_file(), NULL,
880+
"prepare-commit-msg",
877881
git_path_merge_msg(the_repository), "merge", NULL))
878882
abort_commit(remoteheads, NULL);
879883
if (0 < option_edit) {
@@ -882,7 +886,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)
882886
}
883887

884888
if (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),
885-
"commit-msg",
889+
NULL, "commit-msg",
886890
git_path_merge_msg(the_repository), NULL))
887891
abort_commit(remoteheads, NULL);
888892

builtin/receive-pack.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,10 +1439,12 @@ static const char *push_to_deploy(unsigned char *sha1,
14391439
static const char *push_to_checkout_hook = "push-to-checkout";
14401440

14411441
static const char *push_to_checkout(unsigned char *hash,
1442+
int *invoked_hook,
14421443
struct strvec *env,
14431444
const char *work_tree)
14441445
{
14451446
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
1447+
opt.invoked_hook = invoked_hook;
14461448

14471449
strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
14481450
strvec_pushv(&opt.env, env->v);
@@ -1460,6 +1462,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w
14601462
{
14611463
const char *retval, *work_tree, *git_dir = NULL;
14621464
struct strvec env = STRVEC_INIT;
1465+
int invoked_hook = 0;
14631466

14641467
if (worktree && worktree->path)
14651468
work_tree = worktree->path;
@@ -1477,10 +1480,9 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w
14771480

14781481
strvec_pushf(&env, "GIT_DIR=%s", absolute_path(git_dir));
14791482

1480-
if (!hook_exists(push_to_checkout_hook))
1483+
retval = push_to_checkout(sha1, &invoked_hook, &env, work_tree);
1484+
if (!invoked_hook)
14811485
retval = push_to_deploy(sha1, &env, work_tree);
1482-
else
1483-
retval = push_to_checkout(sha1, &env, work_tree);
14841486

14851487
strvec_clear(&env);
14861488
return retval;

commit.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,7 @@ size_t ignore_non_trailer(const char *buf, size_t len)
16971697
}
16981698

16991699
int run_commit_hook(int editor_is_used, const char *index_file,
1700+
int *invoked_hook,
17001701
const char *name, ...)
17011702
{
17021703
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;

commit.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)
363363
int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);
364364

365365
LAST_ARG_MUST_BE_NULL
366-
int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);
366+
int run_commit_hook(int editor_is_used, const char *index_file,
367+
int *invoked_hook, const char *name, ...);
367368

368369
/* Sign a commit or tag buffer, storing the result in a header. */
369370
int sign_with_header(struct strbuf *buf, const char *keyid);

hook.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ static int notify_hook_finished(int result,
138138
/* |= rc in cb */
139139
hook_cb->rc |= result;
140140

141+
if (hook_cb->invoked_hook)
142+
*hook_cb->invoked_hook = 1;
143+
141144
return 1;
142145
}
143146

@@ -152,6 +155,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,
152155
.rc = 0,
153156
.hook_name = hook_name,
154157
.options = options,
158+
.invoked_hook = options->invoked_hook,
155159
};
156160
if (options->absolute_path) {
157161
strbuf_add_absolute_path(&abs_path, hook_path);

hook.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ struct run_hooks_opt
5757
* for an example.
5858
*/
5959
consume_sideband_fn consume_sideband;
60+
61+
/*
62+
* A pointer which if provided will be set to 1 or 0 depending
63+
* on if a hook was invoked (i.e. existed), regardless of
64+
* whether or not that was successful. Used for avoiding
65+
* TOCTOU races in code that would otherwise call hook_exist()
66+
* after a "maybe hook run" to see if a hook was invoked.
67+
*/
68+
int *invoked_hook;
6069
};
6170

6271
#define RUN_HOOKS_OPT_INIT { \
@@ -81,6 +90,7 @@ struct hook_cb_data {
8190
const char *hook_name;
8291
struct hook *run_me;
8392
struct run_hooks_opt *options;
93+
int *invoked_hook;
8494
};
8595

8696
/*

sequencer.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,7 +1204,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,
12041204
} else {
12051205
arg1 = "message";
12061206
}
1207-
if (run_commit_hook(0, r->index_file, "prepare-commit-msg", name,
1207+
if (run_commit_hook(0, r->index_file, NULL, "prepare-commit-msg", name,
12081208
arg1, arg2, NULL))
12091209
ret = error(_("'prepare-commit-msg' hook failed"));
12101210

@@ -1534,7 +1534,7 @@ static int try_to_commit(struct repository *r,
15341534
goto out;
15351535
}
15361536

1537-
run_commit_hook(0, r->index_file, "post-commit", NULL);
1537+
run_commit_hook(0, r->index_file, NULL, "post-commit", NULL);
15381538
if (flags & AMEND_MSG)
15391539
commit_post_rewrite(r, current_head, oid);
15401540

0 commit comments

Comments
 (0)