{"thread":{"id":"55346","subject":"[PATCH 0/2] checkout: don't follow symlinks when removing entries","startedAt":"2021-03-18T18:44:49Z","lastAt":"2021-03-18T18:44:50Z","messageCount":3,"participants":["Matheus Tavares"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"419643","messageId":"cover.1616091229.git.matheus.bernardino@usp.br","threadId":"55346","inReplyTo":null,"subject":"[PATCH 0/2] checkout: don't follow symlinks when removing entries","fromName":"Matheus Tavares","fromEmail":"matheus.bernardino@usp.br","sentAt":"2021-03-18T18:43:45Z","receivedAt":"2021-03-18T18:44:49Z","isPatch":true,"sender":{"key":"matheus.tavb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701583?v=4"},"body":"If the leading path of a tracked file gets replaced by a symlink, git\ncheckout will follow that symlink when removing the file. This bug seems\nto be present since 2011. Fix that and add a regression test.\n\nMatheus Tavares (2):\n  symlinks: update comment on threaded_check_leading_path()\n  checkout: don't follow symlinks when removing entries\n\n cache.h                       |  2 +-\n entry.c                       |  2 +-\n symlinks.c                    | 54 ++++++++++++++++-------------------\n t/t2021-checkout-overwrite.sh | 12 ++++++++\n unpack-trees.c                |  2 +-\n 5 files changed, 39 insertions(+), 33 deletions(-)\n\n-- \n2.30.1\n\n"},{"id":"419644","messageId":"ec8d5f86994d91b6dc430ff2e1b79a7cdf8120f9.1616091229.git.matheus.bernardino@usp.br","threadId":"55346","inReplyTo":"cover.1616091229.git.matheus.bernardino@usp.br","subject":"[PATCH 1/2] symlinks: update comment on threaded_check_leading_path()","fromName":"Matheus Tavares","fromEmail":"matheus.bernardino@usp.br","sentAt":"2021-03-18T18:43:46Z","receivedAt":"2021-03-18T18:44:50Z","isPatch":true,"sender":{"key":"matheus.tavb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701583?v=4"},"body":"Since 1d718a5108 (\"do not overwrite untracked symlinks\", 2011-02-20),\nthe comment on top of threaded_check_leading_path() is outdated and no\nlonger reflects the behavior of this function. Let's updated it to avoid\nconfusions. While we are here, also remove some duplicated comments to\navoid similar maintenance problems.\n\nSigned-off-by: Matheus Tavares <matheus.bernardino@usp.br>\n---\n symlinks.c | 26 +++-----------------------\n 1 file changed, 3 insertions(+), 23 deletions(-)\n\ndiff --git a/symlinks.c b/symlinks.c\nindex 7dbb6b23d9..fbccd340f0 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -202,36 +202,23 @@ int threaded_has_symlink_leading_path(struct cache_def *cache, const char *name,\n \treturn lstat_cache(cache, name, len, FL_SYMLINK|FL_DIR, USE_ONLY_LSTAT) & FL_SYMLINK;\n }\n \n-/*\n- * Return non-zero if path 'name' has a leading symlink component\n- */\n int has_symlink_leading_path(const char *name, int len)\n {\n \treturn threaded_has_symlink_leading_path(&default_cache, name, len);\n }\n \n-/*\n- * Return zero if path 'name' has a leading symlink component or\n- * if some leading path component does not exists.\n- *\n- * Return -1 if leading path exists and is a directory.\n- *\n- * Return path length if leading path exists and is neither a\n- * directory nor a symlink.\n- */\n int check_leading_path(const char *name, int len)\n {\n \treturn threaded_check_leading_path(&default_cache, name, len);\n }\n \n /*\n- * Return zero if path 'name' has a leading symlink component or\n- * if some leading path component does not exists.\n+ * Return zero if some leading path component of 'name' does not exist.\n  *\n  * Return -1 if leading path exists and is a directory.\n  *\n- * Return path length if leading path exists and is neither a\n- * directory nor a symlink.\n+ * Return the length of a leading component if it either exists but it's not a\n+ * directory, or if we were unable to lstat() it.\n  */\n static int threaded_check_leading_path(struct cache_def *cache, const char *name, int len)\n {\n@@ -246,13 +233,6 @@ static int threaded_check_leading_path(struct cache_def *cache, const char *name\n \t\treturn match_len;\n }\n \n-/*\n- * Return non-zero if all path components of 'name' exists as a\n- * directory.  If prefix_len > 0, we will test with the stat()\n- * function instead of the lstat() function for a prefix length of\n- * 'prefix_len', thus we then allow for symlinks in the prefix part as\n- * long as those points to real existing directories.\n- */\n int has_dirs_only_path(const char *name, int len, int prefix_len)\n {\n \treturn threaded_has_dirs_only_path(&default_cache, name, len, prefix_len);\n-- \n2.30.1\n\n"},{"id":"419645","messageId":"b7c98d5034fe77270971c57896f722640cbce7d8.1616091229.git.matheus.bernardino@usp.br","threadId":"55346","inReplyTo":"cover.1616091229.git.matheus.bernardino@usp.br","subject":"[PATCH 2/2] checkout: don't follow symlinks when removing entries","fromName":"Matheus Tavares","fromEmail":"matheus.bernardino@usp.br","sentAt":"2021-03-18T18:43:47Z","receivedAt":"2021-03-18T18:44:50Z","isPatch":true,"sender":{"key":"matheus.tavb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/12701583?v=4"},"body":"At 1d718a5108 (\"do not overwrite untracked symlinks\", 2011-02-20),\nsymlink.c:check_leading_path() started returning different codes for\nFL_ENOENT and FL_SYMLINK. But one of its callers, unlink_entry(), was\nnot adjusted for this change, so it started to follow symlinks on the\nleading path of to-be-removed entries. Fix that and add a regression\ntest.\n\nNote that since 1d718a5108 check_leading_path() no longer differentiates\nthe case where it found a symlink in the path's leading components from\nthe cases where it found a regular file or failed to lstat() the\ncomponent. So, a side effect of this current patch is that\nunlink_entry() now returns early in all of these three cases. And\nbecause we no longer try to unlink such paths, we also don't get the\nwarning from remove_or_warn().\n\nFor the regular file and symlink cases, it's questionable whether the\nwarning was useful in the first place: unlink_entry() removes tracked\npaths that should no longer be present in the state we are checking out\nto. If the path had its leading dir replaced by another file, it means\nthat the basename already doesn't exist, so there is no need for a\nwarning. Sure, we are leaving a regular file or symlink behind at the\npath's dirname, but this file is either untracked now (so again, no\nneed to warn), or it will be replaced by a tracked file during the next\nphase of this checkout operation.\n\nAs for failing to lstat() one of the leading components, the basename\nmight still exist only we cannot unlink it (e.g. due to the lack of the\nrequired permissions). Since the user expect it to be removed\n(especially with checkout's --no-overlay option), add back the warning\nin this more relevant case.\n\nSigned-off-by: Matheus Tavares <matheus.bernardino@usp.br>\n---\n cache.h                       |  2 +-\n entry.c                       |  2 +-\n symlinks.c                    | 30 ++++++++++++++++++++++--------\n t/t2021-checkout-overwrite.sh | 12 ++++++++++++\n unpack-trees.c                |  2 +-\n 5 files changed, 37 insertions(+), 11 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex 6fda8091f1..4f0b29bfe4 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1659,7 +1659,7 @@ static inline void cache_def_clear(struct cache_def *cache)\n \n int has_symlink_leading_path(const char *name, int len);\n int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);\n-int check_leading_path(const char *name, int len);\n+int check_leading_path(const char *name, int len, int warn_on_lstat_err);\n int has_dirs_only_path(const char *name, int len, int prefix_len);\n void invalidate_lstat_cache(void);\n void schedule_dir_for_removal(const char *name, int len);\ndiff --git a/entry.c b/entry.c\nindex 7b9f43716f..33ce80ca91 100644\n--- a/entry.c\n+++ b/entry.c\n@@ -530,7 +530,7 @@ void unlink_entry(const struct cache_entry *ce)\n \t\tsubmodule_move_head(ce->name, \"HEAD\", NULL,\n \t\t\t\t    SUBMODULE_MOVE_HEAD_FORCE);\n \t}\n-\tif (!check_leading_path(ce->name, ce_namelen(ce)))\n+\tif (check_leading_path(ce->name, ce_namelen(ce), 1) >= 0)\n \t\treturn;\n \tif (remove_or_warn(ce->ce_mode, ce->name))\n \t\treturn;\ndiff --git a/symlinks.c b/symlinks.c\nindex fbccd340f0..5232d02020 100644\n--- a/symlinks.c\n+++ b/symlinks.c\n@@ -1,6 +1,7 @@\n #include \"cache.h\"\n \n-static int threaded_check_leading_path(struct cache_def *cache, const char *name, int len);\n+static int threaded_check_leading_path(struct cache_def *cache, const char *name,\n+\t\t\t\t       int len, int warn_on_lstat_err);\n static int threaded_has_dirs_only_path(struct cache_def *cache, const char *name, int len, int prefix_len);\n \n /*\n@@ -72,7 +73,7 @@ static int lstat_cache_matchlen(struct cache_def *cache,\n \t\t\t\tint prefix_len_stat_func)\n {\n \tint match_len, last_slash, last_slash_dir, previous_slash;\n-\tint save_flags, ret;\n+\tint save_flags, ret, saved_errno = 0;\n \tstruct stat st;\n \n \tif (cache->track_flags != track_flags ||\n@@ -139,6 +140,7 @@ static int lstat_cache_matchlen(struct cache_def *cache,\n \n \t\tif (ret) {\n \t\t\t*ret_flags = FL_LSTATERR;\n+\t\t\tsaved_errno = errno;\n \t\t\tif (errno == ENOENT)\n \t\t\t\t*ret_flags |= FL_NOENT;\n \t\t} else if (S_ISDIR(st.st_mode)) {\n@@ -180,6 +182,8 @@ static int lstat_cache_matchlen(struct cache_def *cache,\n \t} else {\n \t\treset_lstat_cache(cache);\n \t}\n+\tif (saved_errno)\n+\t\terrno = saved_errno;\n \treturn match_len;\n }\n \n@@ -207,9 +211,10 @@ int has_symlink_leading_path(const char *name, int len)\n \treturn threaded_has_symlink_leading_path(&default_cache, name, len);\n }\n \n-int check_leading_path(const char *name, int len)\n+int check_leading_path(const char *name, int len, int warn_on_lstat_err)\n {\n-\treturn threaded_check_leading_path(&default_cache, name, len);\n+\treturn threaded_check_leading_path(&default_cache, name, len,\n+\t\t\t\t\t   warn_on_lstat_err);\n }\n \n /*\n@@ -218,19 +223,28 @@ int check_leading_path(const char *name, int len)\n  * Return -1 if leading path exists and is a directory.\n  *\n  * Return the length of a leading component if it either exists but it's not a\n- * directory, or if we were unable to lstat() it.\n+ * directory, or if we were unable to lstat() it. If warn_on_lstat_err is true,\n+ * also emit a warning for this error.\n  */\n-static int threaded_check_leading_path(struct cache_def *cache, const char *name, int len)\n+static int threaded_check_leading_path(struct cache_def *cache, const char *name,\n+\t\t\t\t       int len, int warn_on_lstat_err)\n {\n \tint flags;\n \tint match_len = lstat_cache_matchlen(cache, name, len, &flags,\n \t\t\t   FL_SYMLINK|FL_NOENT|FL_DIR, USE_ONLY_LSTAT);\n+\tint saved_errno = errno;\n+\n \tif (flags & FL_NOENT)\n \t\treturn 0;\n \telse if (flags & FL_DIR)\n \t\treturn -1;\n-\telse\n-\t\treturn match_len;\n+\tif (warn_on_lstat_err && (flags & FL_LSTATERR)) {\n+\t\tchar *path = xmemdupz(name, match_len);\n+\t\terrno = saved_errno;\n+\t\twarning_errno(_(\"failed to lstat '%s'\"), path);\n+\t\tfree(path);\n+\t}\n+\treturn match_len;\n }\n \n int has_dirs_only_path(const char *name, int len, int prefix_len)\ndiff --git a/t/t2021-checkout-overwrite.sh b/t/t2021-checkout-overwrite.sh\nindex c2ada7de37..70d69263e6 100755\n--- a/t/t2021-checkout-overwrite.sh\n+++ b/t/t2021-checkout-overwrite.sh\n@@ -51,4 +51,16 @@ test_expect_success SYMLINKS 'the symlink remained' '\n \ttest -h a/b\n '\n \n+test_expect_success SYMLINKS 'checkout -f must not follow symlinks when removing entries' '\n+\tgit checkout -f start &&\n+\tmkdir dir &&\n+\t>dir/f &&\n+\tgit add dir/f &&\n+\tgit commit -m \"add dir/f\" &&\n+\tmv dir untracked &&\n+\tln -s untracked dir &&\n+\tgit checkout -f HEAD~ &&\n+\ttest_path_is_file untracked/f\n+'\n+\n test_done\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex eb8fcda31b..ed0db8031d 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2097,7 +2097,7 @@ static int verify_absent_1(const struct cache_entry *ce,\n \tif (o->index_only || o->reset || !o->update)\n \t\treturn 0;\n \n-\tlen = check_leading_path(ce->name, ce_namelen(ce));\n+\tlen = check_leading_path(ce->name, ce_namelen(ce), 0);\n \tif (!len)\n \t\treturn 0;\n \telse if (len > 0) {\n-- \n2.30.1\n\n"}]}