From 97613b9cb91eb97b8a4df547396465f3184ccdef Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:45:56 +0200 Subject: transport-helper: fix leaking helper name When initializing the transport helper in `transport_get()`, we allocate the name of the helper. We neither end up transferring ownership of the name, nor do we free it. The associated memory thus leaks. Fix this memory leak by freeing the string at the calling side in `transport_get()`. `transport_helper_init()` now creates its own copy of the string and thus can free it as required. An alterantive way to fix this would be to transfer ownership of the string passed into `transport_helper_init()`, which would avoid the call to xstrdup(1). But it does make for a more surprising calling convention as we do not typically transfer ownership of strings like this. Mark now-passing tests as leak free. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t0611-reftable-httpd.sh | 1 + t/t5563-simple-http-auth.sh | 1 + t/t5564-http-proxy.sh | 1 + t/t5581-http-curl-verbose.sh | 1 + 4 files changed, 4 insertions(+) (limited to 't') diff --git a/t/t0611-reftable-httpd.sh b/t/t0611-reftable-httpd.sh index 5e05b9c1f2..2805995cc8 100755 --- a/t/t0611-reftable-httpd.sh +++ b/t/t0611-reftable-httpd.sh @@ -2,6 +2,7 @@ test_description='reftable HTTPD tests' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-httpd.sh diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh index 5d5caa3f58..4af796de67 100755 --- a/t/t5563-simple-http-auth.sh +++ b/t/t5563-simple-http-auth.sh @@ -2,6 +2,7 @@ test_description='test http auth header and credential helper interop' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-httpd.sh diff --git a/t/t5564-http-proxy.sh b/t/t5564-http-proxy.sh index 9da5134614..bb35b87071 100755 --- a/t/t5564-http-proxy.sh +++ b/t/t5564-http-proxy.sh @@ -2,6 +2,7 @@ test_description="test fetching through http proxy" +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-httpd.sh diff --git a/t/t5581-http-curl-verbose.sh b/t/t5581-http-curl-verbose.sh index cded79c16b..724f610054 100755 --- a/t/t5581-http-curl-verbose.sh +++ b/t/t5581-http-curl-verbose.sh @@ -4,6 +4,7 @@ test_description='test GIT_CURL_VERBOSE' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-httpd.sh start_httpd -- cgit v1.3 From 94e2aa555e7dab4f5296a8dcd8605d751e02b12d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:46:01 +0200 Subject: strbuf: fix leak when `appendwholeline()` fails with EOF In `strbuf_appendwholeline()` we call `strbuf_getwholeline()` with a temporary buffer. In case the call returns an error we indicate this by returning EOF, but never release the temporary buffer. This can cause a leak though because `strbuf_getwholeline()` calls getline(3). Quoting its documentation: If *lineptr was set to NULL before the call, then the buffer should be freed by the user program even on failure. Consequently, the temporary buffer may hold allocated memory even when the call to `strbuf_getwholeline()` fails. Fix this by releasing the temporary buffer on error. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- strbuf.c | 4 +++- t/t1400-update-ref.sh | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 't') diff --git a/strbuf.c b/strbuf.c index 0d929e4e19..e1076c9891 100644 --- a/strbuf.c +++ b/strbuf.c @@ -691,8 +691,10 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term) int strbuf_appendwholeline(struct strbuf *sb, FILE *fp, int term) { struct strbuf line = STRBUF_INIT; - if (strbuf_getwholeline(&line, fp, term)) + if (strbuf_getwholeline(&line, fp, term)) { + strbuf_release(&line); return EOF; + } strbuf_addbuf(sb, &line); strbuf_release(&line); return 0; diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index ec3443cc87..bbee2783ab 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -4,6 +4,8 @@ # test_description='Test git update-ref and basic ref logging' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh Z=$ZERO_OID -- cgit v1.3 From cc395d6b47e4af59b3e87a64b34dffa79e8dc262 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:46:06 +0200 Subject: checkout: clarify memory ownership in `unique_tracking_name()` The function `unique_tracking_name()` returns an allocated string, but does not clearly indicate this because its return type is `const char *` instead of `char *`. This has led to various callsites where we never free its returned memory at all, which causes memory leaks. Plug those leaks and mark now-passing tests as leak free. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/checkout.c | 14 ++++++++------ builtin/worktree.c | 20 +++++++++++--------- checkout.c | 4 ++-- checkout.h | 6 +++--- t/t2024-checkout-dwim.sh | 1 + t/t2060-switch.sh | 1 + t/t3426-rebase-submodule.sh | 1 + t/t3512-cherry-pick-submodule.sh | 1 + t/t3513-revert-submodule.sh | 1 + t/t3600-rm.sh | 1 + t/t3906-stash-submodule.sh | 1 + t/t4137-apply-submodule.sh | 1 + t/t6041-bisect-submodule.sh | 1 + t/t6438-submodule-directory-file-conflicts.sh | 1 + 14 files changed, 34 insertions(+), 20 deletions(-) (limited to 't') diff --git a/builtin/checkout.c b/builtin/checkout.c index f90a4ca4b7..3cf44b4683 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1275,12 +1275,12 @@ static void setup_new_branch_info_and_source_tree( } } -static const char *parse_remote_branch(const char *arg, - struct object_id *rev, - int could_be_checkout_paths) +static char *parse_remote_branch(const char *arg, + struct object_id *rev, + int could_be_checkout_paths) { int num_matches = 0; - const char *remote = unique_tracking_name(arg, rev, &num_matches); + char *remote = unique_tracking_name(arg, rev, &num_matches); if (remote && could_be_checkout_paths) { die(_("'%s' could be both a local file and a tracking branch.\n" @@ -1316,6 +1316,7 @@ static int parse_branchname_arg(int argc, const char **argv, const char **new_branch = &opts->new_branch; int argcount = 0; const char *arg; + char *remote = NULL; int dash_dash_pos; int has_dash_dash = 0; int i; @@ -1416,8 +1417,8 @@ static int parse_branchname_arg(int argc, const char **argv, recover_with_dwim = 0; if (recover_with_dwim) { - const char *remote = parse_remote_branch(arg, rev, - could_be_checkout_paths); + remote = parse_remote_branch(arg, rev, + could_be_checkout_paths); if (remote) { *new_branch = arg; arg = remote; @@ -1459,6 +1460,7 @@ static int parse_branchname_arg(int argc, const char **argv, argc--; } + free(remote); return argcount; } diff --git a/builtin/worktree.c b/builtin/worktree.c index 7e0868df72..937da6c0ee 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -736,16 +736,14 @@ static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote) return 1; } -static const char *dwim_branch(const char *path, const char **new_branch) +static char *dwim_branch(const char *path, char **new_branch) { int n; int branch_exists; const char *s = worktree_basename(path, &n); - const char *branchname = xstrndup(s, n); + char *branchname = xstrndup(s, n); struct strbuf ref = STRBUF_INIT; - UNLEAK(branchname); - branch_exists = !strbuf_check_branch_ref(&ref, branchname) && refs_ref_exists(get_main_ref_store(the_repository), ref.buf); @@ -756,8 +754,7 @@ static const char *dwim_branch(const char *path, const char **new_branch) *new_branch = branchname; if (guess_remote) { struct object_id oid; - const char *remote = - unique_tracking_name(*new_branch, &oid, NULL); + char *remote = unique_tracking_name(*new_branch, &oid, NULL); return remote; } return NULL; @@ -769,6 +766,8 @@ static int add(int ac, const char **av, const char *prefix) const char *new_branch_force = NULL; char *path; const char *branch; + char *branch_to_free = NULL; + char *new_branch_to_free = NULL; const char *new_branch = NULL; const char *opt_track = NULL; const char *lock_reason = NULL; @@ -859,16 +858,17 @@ static int add(int ac, const char **av, const char *prefix) opts.orphan = dwim_orphan(&opts, !!opt_track, 0); } else if (ac < 2) { /* DWIM: Guess branch name from path. */ - const char *s = dwim_branch(path, &new_branch); + char *s = dwim_branch(path, &new_branch_to_free); if (s) - branch = s; + branch = branch_to_free = s; + new_branch = new_branch_to_free; /* DWIM: Infer --orphan when repo has no refs. */ opts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1); } else if (ac == 2) { struct object_id oid; struct commit *commit; - const char *remote; + char *remote; commit = lookup_commit_reference_by_name(branch); if (!commit) { @@ -923,6 +923,8 @@ static int add(int ac, const char **av, const char *prefix) ret = add_worktree(path, branch, &opts); free(path); + free(branch_to_free); + free(new_branch_to_free); return ret; } diff --git a/checkout.c b/checkout.c index 4256e71a7c..cfaea4bd10 100644 --- a/checkout.c +++ b/checkout.c @@ -45,8 +45,8 @@ static int check_tracking_name(struct remote *remote, void *cb_data) return 0; } -const char *unique_tracking_name(const char *name, struct object_id *oid, - int *dwim_remotes_matched) +char *unique_tracking_name(const char *name, struct object_id *oid, + int *dwim_remotes_matched) { struct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT; const char *default_remote = NULL; diff --git a/checkout.h b/checkout.h index 3c514a5ab4..ba15a13fb3 100644 --- a/checkout.h +++ b/checkout.h @@ -8,8 +8,8 @@ * tracking branch. Return the name of the remote if such a branch * exists, NULL otherwise. */ -const char *unique_tracking_name(const char *name, - struct object_id *oid, - int *dwim_remotes_matched); +char *unique_tracking_name(const char *name, + struct object_id *oid, + int *dwim_remotes_matched); #endif /* CHECKOUT_H */ diff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh index a3b1449ef1..2caada3d83 100755 --- a/t/t2024-checkout-dwim.sh +++ b/t/t2024-checkout-dwim.sh @@ -4,6 +4,7 @@ test_description='checkout Ensures that checkout on an unborn branch does what the user expects' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # Is the current branch "refs/heads/$1"? diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh index c91c4db936..77b2346291 100755 --- a/t/t2060-switch.sh +++ b/t/t2060-switch.sh @@ -5,6 +5,7 @@ test_description='switch basic functionality' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t3426-rebase-submodule.sh b/t/t3426-rebase-submodule.sh index ba069dccbd..94ea88e384 100755 --- a/t/t3426-rebase-submodule.sh +++ b/t/t3426-rebase-submodule.sh @@ -2,6 +2,7 @@ test_description='rebase can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh . "$TEST_DIRECTORY"/lib-rebase.sh diff --git a/t/t3512-cherry-pick-submodule.sh b/t/t3512-cherry-pick-submodule.sh index f22d1ddead..9387a22a9e 100755 --- a/t/t3512-cherry-pick-submodule.sh +++ b/t/t3512-cherry-pick-submodule.sh @@ -5,6 +5,7 @@ test_description='cherry-pick can handle submodules' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh diff --git a/t/t3513-revert-submodule.sh b/t/t3513-revert-submodule.sh index 8bfe3ed246..e178968b40 100755 --- a/t/t3513-revert-submodule.sh +++ b/t/t3513-revert-submodule.sh @@ -2,6 +2,7 @@ test_description='revert can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh index 98259e2ada..31ac31d4bc 100755 --- a/t/t3600-rm.sh +++ b/t/t3600-rm.sh @@ -8,6 +8,7 @@ test_description='Test of the various options to git rm.' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # Setup some files to be removed, some with funny characters diff --git a/t/t3906-stash-submodule.sh b/t/t3906-stash-submodule.sh index 0f7348ec21..0f61f01ef4 100755 --- a/t/t3906-stash-submodule.sh +++ b/t/t3906-stash-submodule.sh @@ -2,6 +2,7 @@ test_description='stash can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh diff --git a/t/t4137-apply-submodule.sh b/t/t4137-apply-submodule.sh index 07d5262537..ebd0d4ad17 100755 --- a/t/t4137-apply-submodule.sh +++ b/t/t4137-apply-submodule.sh @@ -2,6 +2,7 @@ test_description='git apply handling submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh diff --git a/t/t6041-bisect-submodule.sh b/t/t6041-bisect-submodule.sh index 82013fc903..3946e18089 100755 --- a/t/t6041-bisect-submodule.sh +++ b/t/t6041-bisect-submodule.sh @@ -2,6 +2,7 @@ test_description='bisect can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh diff --git a/t/t6438-submodule-directory-file-conflicts.sh b/t/t6438-submodule-directory-file-conflicts.sh index 8df67a0ef9..3594190af8 100755 --- a/t/t6438-submodule-directory-file-conflicts.sh +++ b/t/t6438-submodule-directory-file-conflicts.sh @@ -2,6 +2,7 @@ test_description='merge can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh -- cgit v1.3 From 49eb597ce08de7fc4837155fa7910dace92b9ae6 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:46:44 +0200 Subject: config: plug various memory leaks Now that memory ownership rules around `git_config_string()` and `git_config_pathname()` are clearer, it also got easier to spot that the returned memory needs to be free'd. Plug a subset of those cases and mark now-passing tests as leak free. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- alias.c | 4 +++- config.c | 36 +++++++++++++++++++++------- t/t1306-xdg-files.sh | 1 + t/t1350-config-hooks-path.sh | 1 + t/t3415-rebase-autosquash.sh | 1 + t/t4041-diff-submodule-option.sh | 1 + t/t4060-diff-submodule-option-diff-format.sh | 1 + t/t4210-log-i18n.sh | 2 ++ t/t6006-rev-list-format.sh | 1 + t/t7005-editor.sh | 1 + t/t7102-reset.sh | 1 + t/t9129-git-svn-i18n-commitencoding.sh | 1 - t/t9139-git-svn-non-utf8-commitencoding.sh | 1 - 13 files changed, 40 insertions(+), 12 deletions(-) (limited to 't') diff --git a/alias.c b/alias.c index 269892c356..4daafd9bda 100644 --- a/alias.c +++ b/alias.c @@ -21,9 +21,11 @@ static int config_alias_cb(const char *key, const char *value, return 0; if (data->alias) { - if (!strcasecmp(p, data->alias)) + if (!strcasecmp(p, data->alias)) { + FREE_AND_NULL(data->v); return git_config_string(&data->v, key, value); + } } else if (data->list) { string_list_append(data->list, p); } diff --git a/config.c b/config.c index da52a7f8a1..496cd1a61e 100644 --- a/config.c +++ b/config.c @@ -1414,8 +1414,10 @@ static int git_default_core_config(const char *var, const char *value, return 0; } - if (!strcmp(var, "core.attributesfile")) + if (!strcmp(var, "core.attributesfile")) { + FREE_AND_NULL(git_attributes_file); return git_config_pathname(&git_attributes_file, var, value); + } if (!strcmp(var, "core.hookspath")) { if (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL && @@ -1428,6 +1430,7 @@ static int git_default_core_config(const char *var, const char *value, "again with " "`GIT_CLONE_PROTECTION_ACTIVE=false`"), value); + FREE_AND_NULL(git_hooks_path); return git_config_pathname(&git_hooks_path, var, value); } @@ -1576,8 +1579,10 @@ static int git_default_core_config(const char *var, const char *value, return 0; } - if (!strcmp(var, "core.editor")) + if (!strcmp(var, "core.editor")) { + FREE_AND_NULL(editor_program); return git_config_string(&editor_program, var, value); + } if (!strcmp(var, "core.commentchar") || !strcmp(var, "core.commentstring")) { @@ -1595,11 +1600,13 @@ static int git_default_core_config(const char *var, const char *value, return 0; } - if (!strcmp(var, "core.askpass")) + if (!strcmp(var, "core.askpass")) { + FREE_AND_NULL(askpass_program); return git_config_string(&askpass_program, var, value); + } if (!strcmp(var, "core.excludesfile")) { - free(excludes_file); + FREE_AND_NULL(excludes_file); return git_config_pathname(&excludes_file, var, value); } @@ -1702,11 +1709,15 @@ static int git_default_sparse_config(const char *var, const char *value) static int git_default_i18n_config(const char *var, const char *value) { - if (!strcmp(var, "i18n.commitencoding")) + if (!strcmp(var, "i18n.commitencoding")) { + FREE_AND_NULL(git_commit_encoding); return git_config_string(&git_commit_encoding, var, value); + } - if (!strcmp(var, "i18n.logoutputencoding")) + if (!strcmp(var, "i18n.logoutputencoding")) { + FREE_AND_NULL(git_log_output_encoding); return git_config_string(&git_log_output_encoding, var, value); + } /* Add other config variables here and to Documentation/config.txt. */ return 0; @@ -1779,10 +1790,15 @@ static int git_default_push_config(const char *var, const char *value) static int git_default_mailmap_config(const char *var, const char *value) { - if (!strcmp(var, "mailmap.file")) + if (!strcmp(var, "mailmap.file")) { + FREE_AND_NULL(git_mailmap_file); return git_config_pathname(&git_mailmap_file, var, value); - if (!strcmp(var, "mailmap.blob")) + } + + if (!strcmp(var, "mailmap.blob")) { + FREE_AND_NULL(git_mailmap_blob); return git_config_string(&git_mailmap_blob, var, value); + } /* Add other config variables here and to Documentation/config.txt. */ return 0; @@ -1790,8 +1806,10 @@ static int git_default_mailmap_config(const char *var, const char *value) static int git_default_attr_config(const char *var, const char *value) { - if (!strcmp(var, "attr.tree")) + if (!strcmp(var, "attr.tree")) { + FREE_AND_NULL(git_attr_tree); return git_config_string(&git_attr_tree, var, value); + } /* * Add other attribute related config variables here and to diff --git a/t/t1306-xdg-files.sh b/t/t1306-xdg-files.sh index 40d3c42618..53e5b290b9 100755 --- a/t/t1306-xdg-files.sh +++ b/t/t1306-xdg-files.sh @@ -7,6 +7,7 @@ test_description='Compatibility with $XDG_CONFIG_HOME/git/ files' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'read config: xdg file exists and ~/.gitconfig doesn'\''t' ' diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh index f6dc83e2aa..5c47f9ecc3 100755 --- a/t/t1350-config-hooks-path.sh +++ b/t/t1350-config-hooks-path.sh @@ -2,6 +2,7 @@ test_description='Test the core.hooksPath configuration variable' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'set up a pre-commit hook in core.hooksPath' ' diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index fcc40d6fe1..22452ff84c 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -5,6 +5,7 @@ test_description='auto squash' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-rebase.sh diff --git a/t/t4041-diff-submodule-option.sh b/t/t4041-diff-submodule-option.sh index 0c1502d4b0..8fc40e75eb 100755 --- a/t/t4041-diff-submodule-option.sh +++ b/t/t4041-diff-submodule-option.sh @@ -12,6 +12,7 @@ This test tries to verify the sanity of the --submodule option of git diff. GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # Tested non-UTF-8 encoding diff --git a/t/t4060-diff-submodule-option-diff-format.sh b/t/t4060-diff-submodule-option-diff-format.sh index 97c6424cd5..8ce67442d9 100755 --- a/t/t4060-diff-submodule-option-diff-format.sh +++ b/t/t4060-diff-submodule-option-diff-format.sh @@ -10,6 +10,7 @@ test_description='Support for diff format verbose submodule difference in git di This test tries to verify the sanity of --submodule=diff option of git diff. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # Tested non-UTF-8 encoding diff --git a/t/t4210-log-i18n.sh b/t/t4210-log-i18n.sh index 75216f19ce..7120030b5c 100755 --- a/t/t4210-log-i18n.sh +++ b/t/t4210-log-i18n.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='test log with i18n features' + +TEST_PASSES_SANITIZE_LEAK=true . ./lib-gettext.sh # two forms of é diff --git a/t/t6006-rev-list-format.sh b/t/t6006-rev-list-format.sh index 573eb97a0f..f1623b1c06 100755 --- a/t/t6006-rev-list-format.sh +++ b/t/t6006-rev-list-format.sh @@ -8,6 +8,7 @@ test_description='git rev-list --pretty=format test' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-terminal.sh diff --git a/t/t7005-editor.sh b/t/t7005-editor.sh index 5fcf281dfb..b9822294fe 100755 --- a/t/t7005-editor.sh +++ b/t/t7005-editor.sh @@ -2,6 +2,7 @@ test_description='GIT_EDITOR, core.editor, and stuff' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh unset EDITOR VISUAL GIT_EDITOR diff --git a/t/t7102-reset.sh b/t/t7102-reset.sh index 62d9f846ce..2add26d768 100755 --- a/t/t7102-reset.sh +++ b/t/t7102-reset.sh @@ -10,6 +10,7 @@ Documented tests for git reset' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh commit_msg () { diff --git a/t/t9129-git-svn-i18n-commitencoding.sh b/t/t9129-git-svn-i18n-commitencoding.sh index 185248a4cd..01e1e8a8f7 100755 --- a/t/t9129-git-svn-i18n-commitencoding.sh +++ b/t/t9129-git-svn-i18n-commitencoding.sh @@ -4,7 +4,6 @@ test_description='git svn honors i18n.commitEncoding in config' -TEST_FAILS_SANITIZE_LEAK=true . ./lib-git-svn.sh compare_git_head_with () { diff --git a/t/t9139-git-svn-non-utf8-commitencoding.sh b/t/t9139-git-svn-non-utf8-commitencoding.sh index b7f756b2b7..22d80b0be2 100755 --- a/t/t9139-git-svn-non-utf8-commitencoding.sh +++ b/t/t9139-git-svn-non-utf8-commitencoding.sh @@ -4,7 +4,6 @@ test_description='git svn refuses to dcommit non-UTF8 messages' -TEST_FAILS_SANITIZE_LEAK=true . ./lib-git-svn.sh # ISO-2022-JP can pass for valid UTF-8, so skipping that in this test -- cgit v1.3 From 96c1655095ae21040afe9d9c05cf42bb0fc03581 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:46:49 +0200 Subject: builtin/credential: clear credential before exit We never release memory associated with `struct credential`. Fix this and mark the corresponding test as leak free. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/credential.c | 2 ++ t/t0300-credentials.sh | 2 ++ 2 files changed, 4 insertions(+) (limited to 't') diff --git a/builtin/credential.c b/builtin/credential.c index 5100d441f2..b72e76dd9a 100644 --- a/builtin/credential.c +++ b/builtin/credential.c @@ -39,5 +39,7 @@ int cmd_credential(int argc, const char **argv, const char *prefix UNUSED) } else { usage(usage_msg); } + + credential_clear(&c); return 0; } diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh index 432f029d48..6a76b7fdbd 100755 --- a/t/t0300-credentials.sh +++ b/t/t0300-credentials.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='basic credential helper tests' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-credential.sh -- cgit v1.3 From ba9d029445e183d7c7dda75887cee1b5d6fee1d7 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:46:54 +0200 Subject: commit-reach: fix memory leak in `ahead_behind()` We use a priority queue in `ahead_behind()` to compute the ahead/behind count for commits. We may not iterate through all commits part of that queue though in case all of its entries are stale. Consequently, as we never make the effort to release the remaining commits, we end up leaking bit arrays that we have allocated for each of the contained commits. Plug this leak and mark the corresponding test as leak free. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- commit-reach.c | 4 ++++ t/t3203-branch-output.sh | 2 ++ 2 files changed, 6 insertions(+) (limited to 't') diff --git a/commit-reach.c b/commit-reach.c index 8f9b008f87..384aee1ab3 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -1106,6 +1106,10 @@ void ahead_behind(struct repository *r, /* STALE is used here, PARENT2 is used by insert_no_dup(). */ repo_clear_commit_marks(r, PARENT2 | STALE); + while (prio_queue_peek(&queue)) { + struct commit *c = prio_queue_get(&queue); + free_bit_array(c); + } clear_bit_arrays(&bit_arrays); clear_prio_queue(&queue); } diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh index 758963b189..e627f08a17 100755 --- a/t/t3203-branch-output.sh +++ b/t/t3203-branch-output.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='git branch display tests' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-terminal.sh -- cgit v1.3 From 3ef52dd1125b6c9223fd03aca146f6b799e014f9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:46:59 +0200 Subject: submodule: fix leaking memory for submodule entries In `free_one_config()` we never end up freeing the `url` and `ignore` fields and thus leak memory. Fix those leaks and mark now-passing tests as leak free. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- submodule-config.c | 2 ++ t/t1013-read-tree-submodule.sh | 1 + t/t2013-checkout-submodule.sh | 1 + t/t3007-ls-files-recurse-submodules.sh | 1 + t/t7112-reset-submodule.sh | 1 + 5 files changed, 6 insertions(+) (limited to 't') diff --git a/submodule-config.c b/submodule-config.c index 11428b4ada..ec45ea67b9 100644 --- a/submodule-config.c +++ b/submodule-config.c @@ -91,6 +91,8 @@ static void free_one_config(struct submodule_entry *entry) free((void *) entry->config->path); free((void *) entry->config->name); free((void *) entry->config->branch); + free((void *) entry->config->url); + free((void *) entry->config->ignore); free((void *) entry->config->update_strategy.command); free(entry->config); } diff --git a/t/t1013-read-tree-submodule.sh b/t/t1013-read-tree-submodule.sh index bfc90d4cf2..cf8b94ebed 100755 --- a/t/t1013-read-tree-submodule.sh +++ b/t/t1013-read-tree-submodule.sh @@ -2,6 +2,7 @@ test_description='read-tree can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh diff --git a/t/t2013-checkout-submodule.sh b/t/t2013-checkout-submodule.sh index b2bdd1fcb4..3c1d663d94 100755 --- a/t/t2013-checkout-submodule.sh +++ b/t/t2013-checkout-submodule.sh @@ -2,6 +2,7 @@ test_description='checkout can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh diff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh index 61771eec83..f04bdc8c78 100755 --- a/t/t3007-ls-files-recurse-submodules.sh +++ b/t/t3007-ls-files-recurse-submodules.sh @@ -6,6 +6,7 @@ This test verifies the recurse-submodules feature correctly lists files from submodules. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup directory structure and submodules' ' diff --git a/t/t7112-reset-submodule.sh b/t/t7112-reset-submodule.sh index a3e2413bc3..b0d3d93b0b 100755 --- a/t/t7112-reset-submodule.sh +++ b/t/t7112-reset-submodule.sh @@ -2,6 +2,7 @@ test_description='reset can handle submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh -- cgit v1.3 From 11ce77b5cc04e2a42b98f0f9f42d367f50f3b1fc Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:47:04 +0200 Subject: strvec: add functions to replace and remove strings Add two functions that allow to replace and remove strings contained in the strvec. This will be used by a subsequent commit that refactors git-mv(1). While at it, add a bunch of unit tests that cover both old and new functionality. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Makefile | 1 + strvec.c | 20 ++++ strvec.h | 13 +++ t/unit-tests/t-strvec.c | 269 ++++++++++++++++++++++++++++++++++++++++++++++++ t/unit-tests/test-lib.c | 13 +++ t/unit-tests/test-lib.h | 13 +++ 6 files changed, 329 insertions(+) create mode 100644 t/unit-tests/t-strvec.c (limited to 't') diff --git a/Makefile b/Makefile index cf504963c2..d4000fb1d6 100644 --- a/Makefile +++ b/Makefile @@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/% UNIT_TEST_PROGRAMS += t-mem-pool UNIT_TEST_PROGRAMS += t-strbuf +UNIT_TEST_PROGRAMS += t-strvec UNIT_TEST_PROGRAMS += t-ctype UNIT_TEST_PROGRAMS += t-prio-queue UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS)) diff --git a/strvec.c b/strvec.c index 178f4f3748..d4073ec9fa 100644 --- a/strvec.c +++ b/strvec.c @@ -56,6 +56,26 @@ void strvec_pushv(struct strvec *array, const char **items) strvec_push(array, *items); } +const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement) +{ + char *to_free; + if (idx >= array->nr) + BUG("index outside of array boundary"); + to_free = (char *) array->v[idx]; + array->v[idx] = xstrdup(replacement); + free(to_free); + return array->v[idx]; +} + +void strvec_remove(struct strvec *array, size_t idx) +{ + if (idx >= array->nr) + BUG("index outside of array boundary"); + free((char *)array->v[idx]); + memmove(array->v + idx, array->v + idx + 1, (array->nr - idx) * sizeof(char *)); + array->nr--; +} + void strvec_pop(struct strvec *array) { if (!array->nr) diff --git a/strvec.h b/strvec.h index 4715d3e51f..6c7e8b7d50 100644 --- a/strvec.h +++ b/strvec.h @@ -64,6 +64,19 @@ void strvec_pushl(struct strvec *, ...); /* Push a null-terminated array of strings onto the end of the array. */ void strvec_pushv(struct strvec *, const char **); +/** + * Replace the value at the given index with a new value. The index must be + * valid. Returns a pointer to the inserted value. + */ +const char *strvec_replace(struct strvec *array, size_t idx, const char *replacement); + +/* + * Remove the value at the given index. The remainder of the array will be + * moved to fill the resulting gap. The provided index must point into the + * array. + */ +void strvec_remove(struct strvec *array, size_t idx); + /** * Remove the final element from the array. If there are no * elements in the array, do nothing. diff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c new file mode 100644 index 0000000000..f17fb10d9e --- /dev/null +++ b/t/unit-tests/t-strvec.c @@ -0,0 +1,269 @@ +#include "test-lib.h" +#include "strbuf.h" +#include "strvec.h" + +#define check_strvec(vec, ...) \ + check_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__) +static void check_strvec_loc(const char *loc, struct strvec *vec, ...) +{ + va_list ap; + size_t nr = 0; + + va_start(ap, vec); + while (1) { + const char *str = va_arg(ap, const char *); + if (!str) + break; + + if (!check_uint(vec->nr, >, nr) || + !check_uint(vec->alloc, >, nr) || + !check_str(vec->v[nr], str)) { + struct strbuf msg = STRBUF_INIT; + strbuf_addf(&msg, "strvec index %"PRIuMAX, (uintmax_t) nr); + test_assert(loc, msg.buf, 0); + strbuf_release(&msg); + return; + } + + nr++; + } + + check_uint(vec->nr, ==, nr); + check_uint(vec->alloc, >=, nr); + check_pointer_eq(vec->v[nr], NULL); +} + +static void t_static_init(void) +{ + struct strvec vec = STRVEC_INIT; + check_pointer_eq(vec.v, empty_strvec); + check_uint(vec.nr, ==, 0); + check_uint(vec.alloc, ==, 0); +} + +static void t_dynamic_init(void) +{ + struct strvec vec; + strvec_init(&vec); + check_pointer_eq(vec.v, empty_strvec); + check_uint(vec.nr, ==, 0); + check_uint(vec.alloc, ==, 0); +} + +static void t_clear(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_push(&vec, "foo"); + strvec_clear(&vec); + check_pointer_eq(vec.v, empty_strvec); + check_uint(vec.nr, ==, 0); + check_uint(vec.alloc, ==, 0); +} + +static void t_push(void) +{ + struct strvec vec = STRVEC_INIT; + + strvec_push(&vec, "foo"); + check_strvec(&vec, "foo", NULL); + + strvec_push(&vec, "bar"); + check_strvec(&vec, "foo", "bar", NULL); + + strvec_clear(&vec); +} + +static void t_pushf(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushf(&vec, "foo: %d", 1); + check_strvec(&vec, "foo: 1", NULL); + strvec_clear(&vec); +} + +static void t_pushl(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + check_strvec(&vec, "foo", "bar", "baz", NULL); + strvec_clear(&vec); +} + +static void t_pushv(void) +{ + const char *strings[] = { + "foo", "bar", "baz", NULL, + }; + struct strvec vec = STRVEC_INIT; + + strvec_pushv(&vec, strings); + check_strvec(&vec, "foo", "bar", "baz", NULL); + + strvec_clear(&vec); +} + +static void t_replace_at_head(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + strvec_replace(&vec, 0, "replaced"); + check_strvec(&vec, "replaced", "bar", "baz", NULL); + strvec_clear(&vec); +} + +static void t_replace_at_tail(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + strvec_replace(&vec, 2, "replaced"); + check_strvec(&vec, "foo", "bar", "replaced", NULL); + strvec_clear(&vec); +} + +static void t_replace_in_between(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + strvec_replace(&vec, 1, "replaced"); + check_strvec(&vec, "foo", "replaced", "baz", NULL); + strvec_clear(&vec); +} + +static void t_replace_with_substring(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", NULL); + strvec_replace(&vec, 0, vec.v[0] + 1); + check_strvec(&vec, "oo", NULL); + strvec_clear(&vec); +} + +static void t_remove_at_head(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + strvec_remove(&vec, 0); + check_strvec(&vec, "bar", "baz", NULL); + strvec_clear(&vec); +} + +static void t_remove_at_tail(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + strvec_remove(&vec, 2); + check_strvec(&vec, "foo", "bar", NULL); + strvec_clear(&vec); +} + +static void t_remove_in_between(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + strvec_remove(&vec, 1); + check_strvec(&vec, "foo", "baz", NULL); + strvec_clear(&vec); +} + +static void t_pop_empty_array(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pop(&vec); + check_strvec(&vec, NULL); + strvec_clear(&vec); +} + +static void t_pop_non_empty_array(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_pushl(&vec, "foo", "bar", "baz", NULL); + strvec_pop(&vec); + check_strvec(&vec, "foo", "bar", NULL); + strvec_clear(&vec); +} + +static void t_split_empty_string(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_split(&vec, ""); + check_strvec(&vec, NULL); + strvec_clear(&vec); +} + +static void t_split_single_item(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_split(&vec, "foo"); + check_strvec(&vec, "foo", NULL); + strvec_clear(&vec); +} + +static void t_split_multiple_items(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_split(&vec, "foo bar baz"); + check_strvec(&vec, "foo", "bar", "baz", NULL); + strvec_clear(&vec); +} + +static void t_split_whitespace_only(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_split(&vec, " \t\n"); + check_strvec(&vec, NULL); + strvec_clear(&vec); +} + +static void t_split_multiple_consecutive_whitespaces(void) +{ + struct strvec vec = STRVEC_INIT; + strvec_split(&vec, "foo\n\t bar"); + check_strvec(&vec, "foo", "bar", NULL); + strvec_clear(&vec); +} + +static void t_detach(void) +{ + struct strvec vec = STRVEC_INIT; + const char **detached; + + strvec_push(&vec, "foo"); + + detached = strvec_detach(&vec); + check_str(detached[0], "foo"); + check_pointer_eq(detached[1], NULL); + + check_pointer_eq(vec.v, empty_strvec); + check_uint(vec.nr, ==, 0); + check_uint(vec.alloc, ==, 0); + + free((char *) detached[0]); + free(detached); +} + +int cmd_main(int argc, const char **argv) +{ + TEST(t_static_init(), "static initialization"); + TEST(t_dynamic_init(), "dynamic initialization"); + TEST(t_clear(), "clear"); + TEST(t_push(), "push"); + TEST(t_pushf(), "pushf"); + TEST(t_pushl(), "pushl"); + TEST(t_pushv(), "pushv"); + TEST(t_replace_at_head(), "replace at head"); + TEST(t_replace_in_between(), "replace in between"); + TEST(t_replace_at_tail(), "replace at tail"); + TEST(t_replace_with_substring(), "replace with substring"); + TEST(t_remove_at_head(), "remove at head"); + TEST(t_remove_in_between(), "remove in between"); + TEST(t_remove_at_tail(), "remove at tail"); + TEST(t_pop_empty_array(), "pop with empty array"); + TEST(t_pop_non_empty_array(), "pop with non-empty array"); + TEST(t_split_empty_string(), "split empty string"); + TEST(t_split_single_item(), "split single item"); + TEST(t_split_multiple_items(), "split multiple items"); + TEST(t_split_whitespace_only(), "split whitespace only"); + TEST(t_split_multiple_consecutive_whitespaces(), "split multiple consecutive whitespaces"); + TEST(t_detach(), "detach"); + return test_done(); +} diff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c index 66d6980ffb..3c513ce59a 100644 --- a/t/unit-tests/test-lib.c +++ b/t/unit-tests/test-lib.c @@ -318,6 +318,19 @@ int check_bool_loc(const char *loc, const char *check, int ok) union test__tmp test__tmp[2]; +int check_pointer_eq_loc(const char *loc, const char *check, int ok, + const void *a, const void *b) +{ + int ret = test_assert(loc, check, ok); + + if (!ret) { + test_msg(" left: %p", a); + test_msg(" right: %p", b); + } + + return ret; +} + int check_int_loc(const char *loc, const char *check, int ok, intmax_t a, intmax_t b) { diff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h index a8f07ae0b7..2de6d715d5 100644 --- a/t/unit-tests/test-lib.h +++ b/t/unit-tests/test-lib.h @@ -75,6 +75,18 @@ int test_assert(const char *location, const char *check, int ok); check_bool_loc(TEST_LOCATION(), #x, x) int check_bool_loc(const char *loc, const char *check, int ok); +/* + * Compare two integers. Prints a message with the two values if the + * comparison fails. NB this is not thread safe. + */ +#define check_pointer_eq(a, b) \ + (test__tmp[0].p = (a), test__tmp[1].p = (b), \ + check_pointer_eq_loc(TEST_LOCATION(), #a" == "#b, \ + test__tmp[0].p == test__tmp[1].p, \ + test__tmp[0].p, test__tmp[1].p)) +int check_pointer_eq_loc(const char *loc, const char *check, int ok, + const void *a, const void *b); + /* * Compare two integers. Prints a message with the two values if the * comparison fails. NB this is not thread safe. @@ -136,6 +148,7 @@ union test__tmp { intmax_t i; uintmax_t u; char c; + const void *p; }; extern union test__tmp test__tmp[2]; -- cgit v1.3 From 52a7dab439e53d05adb9edc870c92a446e5a9143 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:47:18 +0200 Subject: builtin/mv: refactor to use `struct strvec` Memory allocation patterns in git-mv(1) are extremely hard to follow: We copy around string pointers into manually-managed arrays, some of which alias each other, but only sometimes, while we also drop some of those strings at other times without ever daring to free them. While this may be my own subjective feeling, it seems like others have given up as the code has multiple calls to `UNLEAK()`. These are not sufficient though, and git-mv(1) is still leaking all over the place even with them. Refactor the code to instead track strings in `struct strvec`. While this has the effect of effectively duplicating some of the strings without an actual need, it is way easier to reason about and fixes all of the aliasing of memory that has been going on. It allows us to get rid of the `UNLEAK()` calls and also fixes leaks that those calls did not paper over. Mark tests which are now leak-free accordingly. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/mv.c | 125 +++++++++++++++---------------- t/t4001-diff-rename.sh | 4 +- t/t4043-diff-rename-binary.sh | 1 + t/t4120-apply-popt.sh | 1 + t/t6400-merge-df.sh | 1 + t/t6412-merge-large-rename.sh | 1 + t/t6426-merge-skip-unneeded-updates.sh | 1 + t/t6429-merge-sequence-rename-caching.sh | 1 + 8 files changed, 68 insertions(+), 67 deletions(-) (limited to 't') diff --git a/builtin/mv.c b/builtin/mv.c index 12dcc0b13c..e461d29ca1 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -20,6 +20,7 @@ #include "read-cache-ll.h" #include "repository.h" #include "setup.h" +#include "strvec.h" #include "submodule.h" #include "entry.h" @@ -38,42 +39,32 @@ enum update_mode { #define DUP_BASENAME 1 #define KEEP_TRAILING_SLASH 2 -static const char **internal_prefix_pathspec(const char *prefix, - const char **pathspec, - int count, unsigned flags) +static void internal_prefix_pathspec(struct strvec *out, + const char *prefix, + const char **pathspec, + int count, unsigned flags) { - int i; - const char **result; int prefixlen = prefix ? strlen(prefix) : 0; - ALLOC_ARRAY(result, count + 1); /* Create an intermediate copy of the pathspec based on the flags */ - for (i = 0; i < count; i++) { - int length = strlen(pathspec[i]); - int to_copy = length; - char *it; + for (int i = 0; i < count; i++) { + size_t length = strlen(pathspec[i]); + size_t to_copy = length; + const char *maybe_basename; + char *trimmed, *prefixed_path; + while (!(flags & KEEP_TRAILING_SLASH) && to_copy > 0 && is_dir_sep(pathspec[i][to_copy - 1])) to_copy--; - it = xmemdupz(pathspec[i], to_copy); - if (flags & DUP_BASENAME) { - result[i] = xstrdup(basename(it)); - free(it); - } else { - result[i] = it; - } - } - result[count] = NULL; + trimmed = xmemdupz(pathspec[i], to_copy); + maybe_basename = (flags & DUP_BASENAME) ? basename(trimmed) : trimmed; + prefixed_path = prefix_path(prefix, prefixlen, maybe_basename); + strvec_push(out, prefixed_path); - /* Prefix the pathspec and free the old intermediate strings */ - for (i = 0; i < count; i++) { - const char *match = prefix_path(prefix, prefixlen, result[i]); - free((char *) result[i]); - result[i] = match; + free(prefixed_path); + free(trimmed); } - - return result; } static char *add_slash(const char *path) @@ -176,7 +167,10 @@ int cmd_mv(int argc, const char **argv, const char *prefix) OPT_BOOL(0, "sparse", &ignore_sparse, N_("allow updating entries outside of the sparse-checkout cone")), OPT_END(), }; - const char **source, **destination, **dest_path, **submodule_gitfile; + struct strvec sources = STRVEC_INIT; + struct strvec dest_paths = STRVEC_INIT; + struct strvec destinations = STRVEC_INIT; + const char **submodule_gitfile; char *dst_w_slash = NULL; const char **src_dir = NULL; int src_dir_nr = 0, src_dir_alloc = 0; @@ -201,7 +195,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix) if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); - source = internal_prefix_pathspec(prefix, argv, argc, 0); + internal_prefix_pathspec(&sources, prefix, argv, argc, 0); CALLOC_ARRAY(modes, argc); /* @@ -212,41 +206,39 @@ int cmd_mv(int argc, const char **argv, const char *prefix) flags = KEEP_TRAILING_SLASH; if (argc == 1 && is_directory(argv[0]) && !is_directory(argv[1])) flags = 0; - dest_path = internal_prefix_pathspec(prefix, argv + argc, 1, flags); - dst_w_slash = add_slash(dest_path[0]); + internal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags); + dst_w_slash = add_slash(dest_paths.v[0]); submodule_gitfile = xcalloc(argc, sizeof(char *)); - if (dest_path[0][0] == '\0') + if (dest_paths.v[0][0] == '\0') /* special case: "." was normalized to "" */ - destination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME); - else if (!lstat(dest_path[0], &st) && - S_ISDIR(st.st_mode)) { - destination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME); + internal_prefix_pathspec(&destinations, dest_paths.v[0], argv, argc, DUP_BASENAME); + else if (!lstat(dest_paths.v[0], &st) && S_ISDIR(st.st_mode)) { + internal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME); + } else if (!path_in_sparse_checkout(dst_w_slash, the_repository->index) && + empty_dir_has_sparse_contents(dst_w_slash)) { + internal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME); + dst_mode = SKIP_WORKTREE_DIR; + } else if (argc != 1) { + die(_("destination '%s' is not a directory"), dest_paths.v[0]); } else { - if (!path_in_sparse_checkout(dst_w_slash, the_repository->index) && - empty_dir_has_sparse_contents(dst_w_slash)) { - destination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME); - dst_mode = SKIP_WORKTREE_DIR; - } else if (argc != 1) { - die(_("destination '%s' is not a directory"), dest_path[0]); - } else { - destination = dest_path; - /* - * is a file outside of sparse-checkout - * cone. Insist on cone mode here for backward - * compatibility. We don't want dst_mode to be assigned - * for a file when the repo is using no-cone mode (which - * is deprecated at this point) sparse-checkout. As - * SPARSE here is only considering cone-mode situation. - */ - if (!path_in_cone_mode_sparse_checkout(destination[0], the_repository->index)) - dst_mode = SPARSE; - } + strvec_pushv(&destinations, dest_paths.v); + + /* + * is a file outside of sparse-checkout + * cone. Insist on cone mode here for backward + * compatibility. We don't want dst_mode to be assigned + * for a file when the repo is using no-cone mode (which + * is deprecated at this point) sparse-checkout. As + * SPARSE here is only considering cone-mode situation. + */ + if (!path_in_cone_mode_sparse_checkout(destinations.v[0], the_repository->index)) + dst_mode = SPARSE; } /* Checking */ for (i = 0; i < argc; i++) { - const char *src = source[i], *dst = destination[i]; + const char *src = sources.v[i], *dst = destinations.v[i]; int length; const char *bad = NULL; int skip_sparse = 0; @@ -330,8 +322,6 @@ dir_check: src_dir[src_dir_nr++] = src; n = argc + last - first; - REALLOC_ARRAY(source, n); - REALLOC_ARRAY(destination, n); REALLOC_ARRAY(modes, n); REALLOC_ARRAY(submodule_gitfile, n); @@ -341,12 +331,16 @@ dir_check: for (j = 0; j < last - first; j++) { const struct cache_entry *ce = the_repository->index->cache[first + j]; const char *path = ce->name; - source[argc + j] = path; - destination[argc + j] = - prefix_path(dst_with_slash, dst_with_slash_len, path + length + 1); + char *prefixed_path = prefix_path(dst_with_slash, dst_with_slash_len, path + length + 1); + + strvec_push(&sources, path); + strvec_push(&destinations, prefixed_path); + memset(modes + argc + j, 0, sizeof(enum update_mode)); modes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX; submodule_gitfile[argc + j] = NULL; + + free(prefixed_path); } free(dst_with_slash); @@ -430,8 +424,8 @@ act_on_entry: remove_entry: if (--argc > 0) { int n = argc - i; - MOVE_ARRAY(source + i, source + i + 1, n); - MOVE_ARRAY(destination + i, destination + i + 1, n); + strvec_remove(&sources, i); + strvec_remove(&destinations, i); MOVE_ARRAY(modes + i, modes + i + 1, n); MOVE_ARRAY(submodule_gitfile + i, submodule_gitfile + i + 1, n); @@ -448,7 +442,7 @@ remove_entry: } for (i = 0; i < argc; i++) { - const char *src = source[i], *dst = destination[i]; + const char *src = sources.v[i], *dst = destinations.v[i]; enum update_mode mode = modes[i]; int pos; int sparse_and_dirty = 0; @@ -576,8 +570,9 @@ out: string_list_clear(&src_for_dst, 0); string_list_clear(&dirty_paths, 0); string_list_clear(&only_match_skip_worktree, 0); - UNLEAK(source); - UNLEAK(dest_path); + strvec_clear(&sources); + strvec_clear(&dest_paths); + strvec_clear(&destinations); free(submodule_gitfile); free(modes); return ret; diff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh index 49c042a38a..cd1931dd55 100755 --- a/t/t4001-diff-rename.sh +++ b/t/t4001-diff-rename.sh @@ -3,9 +3,9 @@ # Copyright (c) 2005 Junio C Hamano # -test_description='Test rename detection in diff engine. +test_description='Test rename detection in diff engine.' -' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh diff --git a/t/t4043-diff-rename-binary.sh b/t/t4043-diff-rename-binary.sh index 2a2cf91352..e486493908 100755 --- a/t/t4043-diff-rename-binary.sh +++ b/t/t4043-diff-rename-binary.sh @@ -5,6 +5,7 @@ test_description='Move a binary file' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh diff --git a/t/t4120-apply-popt.sh b/t/t4120-apply-popt.sh index 697e86c0ff..f788428540 100755 --- a/t/t4120-apply-popt.sh +++ b/t/t4120-apply-popt.sh @@ -5,6 +5,7 @@ test_description='git apply -p handling.' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t6400-merge-df.sh b/t/t6400-merge-df.sh index 3de4ef6bd9..27d6efdc9a 100755 --- a/t/t6400-merge-df.sh +++ b/t/t6400-merge-df.sh @@ -7,6 +7,7 @@ test_description='Test merge with directory/file conflicts' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'prepare repository' ' diff --git a/t/t6412-merge-large-rename.sh b/t/t6412-merge-large-rename.sh index ca018d11f5..d0863a8fb5 100755 --- a/t/t6412-merge-large-rename.sh +++ b/t/t6412-merge-large-rename.sh @@ -4,6 +4,7 @@ test_description='merging with large rename matrix' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh count() { diff --git a/t/t6426-merge-skip-unneeded-updates.sh b/t/t6426-merge-skip-unneeded-updates.sh index b059475ed0..62f0180325 100755 --- a/t/t6426-merge-skip-unneeded-updates.sh +++ b/t/t6426-merge-skip-unneeded-updates.sh @@ -22,6 +22,7 @@ test_description="merge cases" # underscore notation is to differentiate different # files that might be renamed into each other's paths.) +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-merge.sh diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh index 0f39ed0d08..cb1c4ceef7 100755 --- a/t/t6429-merge-sequence-rename-caching.sh +++ b/t/t6429-merge-sequence-rename-caching.sh @@ -2,6 +2,7 @@ test_description="remember regular & dir renames in sequence of merges" +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # -- cgit v1.3 From ebdbefa4fe9f618347124b37d44e517e0c6a3e4c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 27 May 2024 13:47:23 +0200 Subject: builtin/mv: fix leaks for submodule gitfile paths Similar to the preceding commit, we have effectively given tracking memory ownership of submodule gitfile paths. Refactor the code to start tracking allocated strings in a separate `struct strvec` such that we can easily plug those leaks. Mark now-passing tests as leak free. Note that ideally, we wouldn't require two separate data structures to track those paths. But we do need to store `NULL` pointers for the gitfile paths such that we can indicate that its corresponding entries in the other arrays do not have such a path at all. And given that `struct strvec`s cannot store `NULL` pointers we cannot use them to store this information. There is another small gotcha that is easy to miss: you may be wondering why we don't want to store `SUBMODULE_WITH_GITDIR` in the strvec. This is because this is a mere sentinel value and not actually a string at all. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/mv.c | 44 ++++++++++++++++++------------- t/t4059-diff-submodule-not-initialized.sh | 1 + t/t7001-mv.sh | 2 ++ t/t7417-submodule-path-url.sh | 1 + t/t7421-submodule-summary-add.sh | 1 + 5 files changed, 30 insertions(+), 19 deletions(-) (limited to 't') diff --git a/builtin/mv.c b/builtin/mv.c index e461d29ca1..81ca910de6 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -82,21 +82,23 @@ static char *add_slash(const char *path) #define SUBMODULE_WITH_GITDIR ((const char *)1) -static void prepare_move_submodule(const char *src, int first, - const char **submodule_gitfile) +static const char *submodule_gitfile_path(const char *src, int first) { struct strbuf submodule_dotgit = STRBUF_INIT; + const char *path; + if (!S_ISGITLINK(the_repository->index->cache[first]->ce_mode)) die(_("Directory %s is in index and no submodule?"), src); if (!is_staging_gitmodules_ok(the_repository->index)) die(_("Please stage your changes to .gitmodules or stash them to proceed")); + strbuf_addf(&submodule_dotgit, "%s/.git", src); - *submodule_gitfile = read_gitfile(submodule_dotgit.buf); - if (*submodule_gitfile) - *submodule_gitfile = xstrdup(*submodule_gitfile); - else - *submodule_gitfile = SUBMODULE_WITH_GITDIR; + + path = read_gitfile(submodule_dotgit.buf); strbuf_release(&submodule_dotgit); + if (path) + return path; + return SUBMODULE_WITH_GITDIR; } static int index_range_of_same_dir(const char *src, int length, @@ -170,7 +172,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix) struct strvec sources = STRVEC_INIT; struct strvec dest_paths = STRVEC_INIT; struct strvec destinations = STRVEC_INIT; - const char **submodule_gitfile; + struct strvec submodule_gitfiles_to_free = STRVEC_INIT; + const char **submodule_gitfiles; char *dst_w_slash = NULL; const char **src_dir = NULL; int src_dir_nr = 0, src_dir_alloc = 0; @@ -208,7 +211,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix) flags = 0; internal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags); dst_w_slash = add_slash(dest_paths.v[0]); - submodule_gitfile = xcalloc(argc, sizeof(char *)); + submodule_gitfiles = xcalloc(argc, sizeof(char *)); if (dest_paths.v[0][0] == '\0') /* special case: "." was normalized to "" */ @@ -306,8 +309,10 @@ dir_check: int first = index_name_pos(the_repository->index, src, length), last; if (first >= 0) { - prepare_move_submodule(src, first, - submodule_gitfile + i); + const char *path = submodule_gitfile_path(src, first); + if (path != SUBMODULE_WITH_GITDIR) + path = strvec_push(&submodule_gitfiles_to_free, path); + submodule_gitfiles[i] = path; goto act_on_entry; } else if (index_range_of_same_dir(src, length, &first, &last) < 1) { @@ -323,7 +328,7 @@ dir_check: n = argc + last - first; REALLOC_ARRAY(modes, n); - REALLOC_ARRAY(submodule_gitfile, n); + REALLOC_ARRAY(submodule_gitfiles, n); dst_with_slash = add_slash(dst); dst_with_slash_len = strlen(dst_with_slash); @@ -338,7 +343,7 @@ dir_check: memset(modes + argc + j, 0, sizeof(enum update_mode)); modes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX; - submodule_gitfile[argc + j] = NULL; + submodule_gitfiles[argc + j] = NULL; free(prefixed_path); } @@ -427,8 +432,8 @@ remove_entry: strvec_remove(&sources, i); strvec_remove(&destinations, i); MOVE_ARRAY(modes + i, modes + i + 1, n); - MOVE_ARRAY(submodule_gitfile + i, - submodule_gitfile + i + 1, n); + MOVE_ARRAY(submodule_gitfiles + i, + submodule_gitfiles + i + 1, n); i--; } } @@ -462,12 +467,12 @@ remove_entry: continue; die_errno(_("renaming '%s' failed"), src); } - if (submodule_gitfile[i]) { + if (submodule_gitfiles[i]) { if (!update_path_in_gitmodules(src, dst)) gitmodules_modified = 1; - if (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR) + if (submodule_gitfiles[i] != SUBMODULE_WITH_GITDIR) connect_work_tree_and_git_dir(dst, - submodule_gitfile[i], + submodule_gitfiles[i], 1); } @@ -573,7 +578,8 @@ out: strvec_clear(&sources); strvec_clear(&dest_paths); strvec_clear(&destinations); - free(submodule_gitfile); + strvec_clear(&submodule_gitfiles_to_free); + free(submodule_gitfiles); free(modes); return ret; } diff --git a/t/t4059-diff-submodule-not-initialized.sh b/t/t4059-diff-submodule-not-initialized.sh index d489230df8..668f526303 100755 --- a/t/t4059-diff-submodule-not-initialized.sh +++ b/t/t4059-diff-submodule-not-initialized.sh @@ -9,6 +9,7 @@ This test tries to verify that add_submodule_odb works when the submodule was initialized previously but the checkout has since been removed. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # Tested non-UTF-8 encoding diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh index 879a6dce60..86258f9f43 100755 --- a/t/t7001-mv.sh +++ b/t/t7001-mv.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='git mv in subdirs' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff-data.sh diff --git a/t/t7417-submodule-path-url.sh b/t/t7417-submodule-path-url.sh index 5e3051da8b..dbbb3853dc 100755 --- a/t/t7417-submodule-path-url.sh +++ b/t/t7417-submodule-path-url.sh @@ -4,6 +4,7 @@ test_description='check handling of .gitmodule path with dash' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t7421-submodule-summary-add.sh b/t/t7421-submodule-summary-add.sh index ce64d8b137..479c8fdde1 100755 --- a/t/t7421-submodule-summary-add.sh +++ b/t/t7421-submodule-summary-add.sh @@ -10,6 +10,7 @@ while making sure to add submodules using `git submodule add` instead of `git add` as done in t7401. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' -- cgit v1.3