summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2015-01-29 12:41:22 -0800
committerJunio C Hamano <gitster@pobox.com>2015-02-02 14:49:39 -0800
commit2b71f41fe8a691f115e7b04a9171812c5a316687 (patch)
treebf017b9cd7b91392f8c9f3ddeac1d9dd624dc62e
parent30ad4868238063897e30d0d7da2c1b813a8f54e7 (diff)
downloadgit-jc/wip-safe-apply-take-2.tar.gz
apply: do not touch a file beyond a symbolic linkjc/wip-safe-apply-take-2
Because Git tracks symbolic links as symbolic links, a path that has a symbolic link in its leading part (e.g. path/to/dir/file, where path/to/dir is a symbolic link to somewhere else, be it inside or outside the working tree) can never appear in a patch that validly applies, unless the same patch first removes the symbolic link to allow a directory to be created there. Detect and reject such a patch. Things to note: - Unfortunately, we cannot reuse the has_symlink_leading_path() from dir.c, as that is only about the working tree, but "git apply" can be told to apply the patch only to the index or to both the index and to the working tree. - We cannot directly use has_symlink_leading_path() even when we are applying only to the working tree, as an early patch of a valid input may remove a symbolic link path/to/dir and then a later patch of the input may create a path path/to/dir/file, but "git apply" first checks the input without touching either the index or the working tree. The leading symbolic link check must be done on the interim result we compute in-core (i.e. after the first patch, there is no path/to/dir symbolic link and it is perfectly valid to create path/to/dir/file). Similarly, when an input creates a symbolic link path/to/dir and then creates a file path/to/dir/file, we need to flag it as an error without actually creating path/to/dir symbolic link in the filesystem. Instead, for any patch in the input that leaves a path (i.e. a non deletion) in the result, we check all leading paths against the resulting tree that the patch would create by inspecting all the patches in the input and then the target of patch application (either the index or the working tree). This way, we catch a mischief or a mistake to add a symbolic link path/to/dir and a file path/to/dir/file at the same time, while allowing a valid patch that removes a symbolic link path/to/dir and then adds a file path/to/dir/file. Signed-off-by: Junio C Hamano <gitster@pobox.com>
-rw-r--r--builtin/apply.c109
-rwxr-xr-xt/t4139-apply-escape.sh6
2 files changed, 112 insertions, 3 deletions
diff --git a/builtin/apply.c b/builtin/apply.c
index 60d821c06f..a760d97848 100644
--- a/builtin/apply.c
+++ b/builtin/apply.c
@@ -3486,6 +3486,103 @@ static int check_to_create(const char *new_name, int ok_if_exists)
return 0;
}
+/*
+ * We need to keep track of how symlinks in the preimage are
+ * manipulated by the patches. A patch to add a/b/c where a/b
+ * is a symlink should not be allowed to affect the directory
+ * the symlink points at, but if the same patch removes a/b,
+ * it is perfectly fine, as the patch removes a/b to make room
+ * to create a directory a/b so that a/b/c can be created.
+ */
+static struct string_list symlink_changes;
+#define SYMLINK_GOES_AWAY 01
+#define SYMLINK_IN_RESULT 02
+
+static uintptr_t register_symlink_changes(const char *path, uintptr_t what)
+{
+ struct string_list_item *ent;
+
+ ent = string_list_lookup(&symlink_changes, path);
+ if (!ent) {
+ ent = string_list_insert(&symlink_changes, path);
+ ent->util = (void *)0;
+ }
+ ent->util = (void *)(what | ((uintptr_t)ent->util));
+ return (uintptr_t)ent->util;
+}
+
+static uintptr_t check_symlink_changes(const char *path)
+{
+ struct string_list_item *ent;
+
+ ent = string_list_lookup(&symlink_changes, path);
+ if (!ent)
+ return 0;
+ return (uintptr_t)ent->util;
+}
+
+static void prepare_symlink_changes(struct patch *patch)
+{
+ for ( ; patch; patch = patch->next) {
+ if ((patch->old_name && S_ISLNK(patch->old_mode)) &&
+ (patch->is_rename || patch->is_delete))
+ /* the symlink at patch->old_name is removed */
+ register_symlink_changes(patch->old_name, SYMLINK_GOES_AWAY);
+
+ if (patch->new_name && S_ISLNK(patch->new_mode))
+ /* the symlink at patch->new_name is created or remains */
+ register_symlink_changes(patch->new_name, SYMLINK_IN_RESULT);
+ }
+}
+
+static int path_is_beyond_symlink_1(struct strbuf *name)
+{
+ do {
+ unsigned int change;
+
+ while (--name->len && name->buf[name->len] != '/')
+ ; /* scan backwards */
+ if (!name->len)
+ break;
+ name->buf[name->len] = '\0';
+ change = check_symlink_changes(name->buf);
+ if (change & SYMLINK_IN_RESULT)
+ return 1;
+ if (change & SYMLINK_GOES_AWAY)
+ /*
+ * This cannot be "return 0", because we may
+ * see a new one created at a higher level.
+ */
+ continue;
+
+ /* otherwise, check the preimage */
+ if (check_index) {
+ int pos = cache_name_pos(name->buf, name->len);
+ if (0 <= pos &&
+ S_ISLNK(active_cache[pos]->ce_mode))
+ return 1;
+ } else {
+ struct stat st;
+ if (!lstat(name->buf, &st) && S_ISLNK(st.st_mode))
+ return 1;
+ }
+ } while (1);
+ return 0;
+}
+
+static int path_is_beyond_symlink(const char *name_)
+{
+ int ret;
+ struct strbuf name = STRBUF_INIT;
+
+ assert(*name_ != '\0');
+ strbuf_addstr(&name, name_);
+ ret = path_is_beyond_symlink_1(&name);
+ strbuf_release(&name);
+
+ return ret;
+}
+
static void die_on_unsafe_path(struct patch *patch)
{
const char *old_name = NULL;
@@ -3593,6 +3690,17 @@ static int check_patch(struct patch *patch)
if (!unsafe_paths)
die_on_unsafe_path(patch);
+ /*
+ * An attempt to read from or delete a path that is beyond
+ * a symbolic link will be prevented by load_patch_target()
+ * that is called at the beginning of apply_data(). We need
+ * to make sure that the patch result is not deposited to
+ * a path that is beyond a symbolic link ourselves.
+ */
+ if (!patch->is_delete && path_is_beyond_symlink(patch->new_name))
+ return error(_("affected file '%s' is beyond a symbolic link"),
+ patch->new_name);
+
if (apply_data(patch, &st, ce) < 0)
return error(_("%s: patch does not apply"), name);
patch->rejected = 0;
@@ -3603,6 +3711,7 @@ static int check_patch_list(struct patch *patch)
{
int err = 0;
+ prepare_symlink_changes(patch);
prepare_fn_table(patch);
while (patch) {
if (apply_verbosely)
diff --git a/t/t4139-apply-escape.sh b/t/t4139-apply-escape.sh
index 5e6717935b..25917cc1c2 100755
--- a/t/t4139-apply-escape.sh
+++ b/t/t4139-apply-escape.sh
@@ -98,7 +98,7 @@ test_expect_success 'cannot del file containing .. (index)' '
test_path_is_file ../foo
'
-test_expect_failure 'symlink escape via ..' '
+test_expect_success 'symlink escape via ..' '
{
mkpatch_symlink tmp .. &&
mkpatch_add tmp/foo ../foo
@@ -107,7 +107,7 @@ test_expect_failure 'symlink escape via ..' '
test_path_is_missing ../foo
'
-test_expect_failure 'symlink escape via .. (index)' '
+test_expect_success 'symlink escape via .. (index)' '
{
mkpatch_symlink tmp .. &&
mkpatch_add tmp/foo ../foo
@@ -116,7 +116,7 @@ test_expect_failure 'symlink escape via .. (index)' '
test_path_is_missing ../foo
'
-test_expect_failure 'symlink escape via absolute path' '
+test_expect_success 'symlink escape via absolute path' '
{
mkpatch_symlink tmp "$(pwd)" &&
mkpatch_add tmp/foo ../foo