diff options
-rw-r--r-- | Documentation/RelNotes/1.8.5.6.txt | 34 | ||||
-rw-r--r-- | Documentation/config.txt | 11 | ||||
-rw-r--r-- | Documentation/git.txt | 3 | ||||
-rw-r--r-- | cache.h | 3 | ||||
-rw-r--r-- | config.c | 10 | ||||
-rw-r--r-- | config.mak.uname | 3 | ||||
-rw-r--r-- | environment.c | 10 | ||||
-rw-r--r-- | fsck.c | 4 | ||||
-rw-r--r-- | path.c | 33 | ||||
-rw-r--r-- | read-cache.c | 10 | ||||
-rwxr-xr-x | t/t1014-read-tree-confusing.sh | 62 | ||||
-rwxr-xr-x | t/t1450-fsck.sh | 65 | ||||
-rw-r--r-- | t/test-lib.sh | 6 | ||||
-rw-r--r-- | unpack-trees.c | 10 | ||||
-rw-r--r-- | utf8.c | 64 | ||||
-rw-r--r-- | utf8.h | 8 |
16 files changed, 297 insertions, 39 deletions
diff --git a/Documentation/RelNotes/1.8.5.6.txt b/Documentation/RelNotes/1.8.5.6.txt new file mode 100644 index 0000000000..92ff92b1e6 --- /dev/null +++ b/Documentation/RelNotes/1.8.5.6.txt @@ -0,0 +1,34 @@ +Git v1.8.5.6 Release Notes +========================== + +Fixes since v1.8.5.5 +-------------------- + + * We used to allow committing a path ".Git/config" with Git that is + running on a case sensitive filesystem, but an attempt to check out + such a path with Git that runs on a case insensitive filesystem + would have clobbered ".git/config", which is definitely not what + the user would have expected. Git now prevents you from tracking + a path with ".Git" (in any case combination) as a path component. + + * On Windows, certain path components that are different from ".git" + are mapped to ".git", e.g. "git~1/config" is treated as if it were + ".git/config". HFS+ has a similar issue, where certain unicode + codepoints are ignored, e.g. ".g\u200cit/config" is treated as if + it were ".git/config". Pathnames with these potential issues are + rejected on the affected systems. Git on systems that are not + affected by this issue (e.g. Linux) can also be configured to + reject them to ensure cross platform interoperability of the hosted + projects. + + * "git fsck" notices a tree object that records such a path that can + be confused with ".git", and with receive.fsckObjects configuration + set to true, an attempt to "git push" such a tree object will be + rejected. Such a path may not be a problem on a well behaving + filesystem but in order to protect those on HFS+ and on case + insensitive filesystems, this check is enabled on all platforms. + +A big "thanks!" for bringing this issue to us goes to our friends in +the Mercurial land, namely, Matt Mackall and Augie Fackler. + +Also contains typofixes, documentation updates and trivial code clean-ups. diff --git a/Documentation/config.txt b/Documentation/config.txt index c26a7c8469..7076aa9282 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -234,6 +234,17 @@ core.precomposeunicode:: When false, file names are handled fully transparent by Git, which is backward compatible with older versions of Git. +core.protectHFS:: + If set to true, do not allow checkout of paths that would + be considered equivalent to `.git` on an HFS+ filesystem. + Defaults to `true` on Mac OS, and `false` elsewhere. + +core.protectNTFS:: + If set to true, do not allow checkout of paths that would + cause problems with the NTFS filesystem, e.g. conflict with + 8.3 "short" names. + Defaults to `true` on Windows, and `false` elsewhere. + core.trustctime:: If false, the ctime differences between the index and the working tree are ignored; useful when the inode change time diff --git a/Documentation/git.txt b/Documentation/git.txt index 3d54378f27..7297fe1ea2 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -52,9 +52,10 @@ Documentation for older releases are available here: link:RelNotes/1.9.1.txt[1.9.1], link:RelNotes/1.9.0.txt[1.9.0]. -* link:v1.8.5.5/git.html[documentation for release 1.8.5.5] +* link:v1.8.5.6/git.html[documentation for release 1.8.5.6] * release notes for + link:RelNotes/1.8.5.6.txt[1.8.5.6], link:RelNotes/1.8.5.5.txt[1.8.5.5], link:RelNotes/1.8.5.4.txt[1.8.5.4], link:RelNotes/1.8.5.3.txt[1.8.5.3], @@ -587,6 +587,8 @@ extern int fsync_object_files; extern int core_preload_index; extern int core_apply_sparse_checkout; extern int precomposed_unicode; +extern int protect_hfs; +extern int protect_ntfs; /* * The character that begins a commented line in user-editable file @@ -782,6 +784,7 @@ int longest_ancestor_length(const char *path, struct string_list *prefixes); char *strip_path_suffix(const char *path, const char *suffix); int daemon_avoid_alias(const char *path); int offset_1st_component(const char *path); +extern int is_ntfs_dotgit(const char *name); /* object replacement */ #define LOOKUP_REPLACE_OBJECT 1 @@ -885,6 +885,16 @@ static int git_default_core_config(const char *var, const char *value) return 0; } + if (!strcmp(var, "core.protecthfs")) { + protect_hfs = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "core.protectntfs")) { + protect_ntfs = git_config_bool(var, value); + return 0; + } + /* Add other config variables here and to Documentation/config.txt. */ return 0; } diff --git a/config.mak.uname b/config.mak.uname index efaed94d5d..f3cdcbcddf 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -97,6 +97,7 @@ ifeq ($(uname_S),Darwin) HAVE_DEV_TTY = YesPlease COMPAT_OBJS += compat/precompose_utf8.o BASIC_CFLAGS += -DPRECOMPOSE_UNICODE + BASIC_CFLAGS += -DPROTECT_HFS_DEFAULT=1 endif ifeq ($(uname_S),SunOS) NEEDS_SOCKET = YesPlease @@ -369,6 +370,7 @@ ifeq ($(uname_S),Windows) EXTLIBS = user32.lib advapi32.lib shell32.lib wininet.lib ws2_32.lib PTHREAD_LIBS = lib = + BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1 ifndef DEBUG BASIC_CFLAGS += -GL -Os -MT BASIC_LDFLAGS += -LTCG @@ -513,6 +515,7 @@ ifneq (,$(findstring MINGW,$(uname_S))) COMPAT_OBJS += compat/mingw.o compat/winansi.o \ compat/win32/pthread.o compat/win32/syslog.o \ compat/win32/dirent.o + BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1 BASIC_LDFLAGS += -Wl,--large-address-aware EXTLIBS += -lws2_32 GITLIBS += git.res diff --git a/environment.c b/environment.c index 4a3437d8a6..39a8c6c343 100644 --- a/environment.c +++ b/environment.c @@ -64,6 +64,16 @@ int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ struct startup_info *startup_info; unsigned long pack_size_limit_cfg; +#ifndef PROTECT_HFS_DEFAULT +#define PROTECT_HFS_DEFAULT 0 +#endif +int protect_hfs = PROTECT_HFS_DEFAULT; + +#ifndef PROTECT_NTFS_DEFAULT +#define PROTECT_NTFS_DEFAULT 0 +#endif +int protect_ntfs = PROTECT_NTFS_DEFAULT; + /* * The character that begins a commented line in user-editable file * that is subject to stripspace. @@ -6,6 +6,7 @@ #include "commit.h" #include "tag.h" #include "fsck.h" +#include "utf8.h" static int fsck_walk_tree(struct tree *tree, fsck_walk_func walk, void *data) { @@ -175,7 +176,8 @@ static int fsck_tree(struct tree *item, int strict, fsck_error error_func) has_dot = 1; if (!strcmp(name, "..")) has_dotdot = 1; - if (!strcmp(name, ".git")) + if (!strcasecmp(name, ".git") || is_hfs_dotgit(name) || + is_ntfs_dotgit(name)) has_dotgit = 1; has_zero_pad |= *(char *)desc.buffer == '0'; update_tree_entry(&desc); @@ -830,3 +830,36 @@ int offset_1st_component(const char *path) return 2 + is_dir_sep(path[2]); return is_dir_sep(path[0]); } + +static int only_spaces_and_periods(const char *path, size_t len, size_t skip) +{ + if (len < skip) + return 0; + len -= skip; + path += skip; + while (len-- > 0) { + char c = *(path++); + if (c != ' ' && c != '.') + return 0; + } + return 1; +} + +int is_ntfs_dotgit(const char *name) +{ + int len; + + for (len = 0; ; len++) + if (!name[len] || name[len] == '\\' || is_dir_sep(name[len])) { + if (only_spaces_and_periods(name, len, 4) && + !strncasecmp(name, ".git", 4)) + return 1; + if (only_spaces_and_periods(name, len, 5) && + !strncasecmp(name, "git~1", 5)) + return 1; + if (name[len] != '\\') + return 0; + name += len + 1; + len = -1; + } +} diff --git a/read-cache.c b/read-cache.c index 4b4effd64b..ee07cd610a 100644 --- a/read-cache.c +++ b/read-cache.c @@ -14,6 +14,7 @@ #include "resolve-undo.h" #include "strbuf.h" #include "varint.h" +#include "utf8.h" static struct cache_entry *refresh_cache_entry(struct cache_entry *ce, unsigned int options); @@ -752,9 +753,10 @@ static int verify_dotfile(const char *rest) * shares the path end test with the ".." case. */ case 'g': - if (rest[1] != 'i') + case 'G': + if (rest[1] != 'i' && rest[1] != 'I') break; - if (rest[2] != 't') + if (rest[2] != 't' && rest[2] != 'T') break; rest += 2; /* fallthrough */ @@ -778,6 +780,10 @@ int verify_path(const char *path) return 1; if (is_dir_sep(c)) { inside: + if (protect_hfs && is_hfs_dotgit(path)) + return 0; + if (protect_ntfs && is_ntfs_dotgit(path)) + return 0; c = *path++; if ((c == '.' && !verify_dotfile(path)) || is_dir_sep(c) || c == '\0') diff --git a/t/t1014-read-tree-confusing.sh b/t/t1014-read-tree-confusing.sh new file mode 100755 index 0000000000..2f5a25d503 --- /dev/null +++ b/t/t1014-read-tree-confusing.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +test_description='check that read-tree rejects confusing paths' +. ./test-lib.sh + +test_expect_success 'create base tree' ' + echo content >file && + git add file && + git commit -m base && + blob=$(git rev-parse HEAD:file) && + tree=$(git rev-parse HEAD^{tree}) +' + +test_expect_success 'enable core.protectHFS for rejection tests' ' + git config core.protectHFS true +' + +test_expect_success 'enable core.protectNTFS for rejection tests' ' + git config core.protectNTFS true +' + +while read path pretty; do + : ${pretty:=$path} + case "$path" in + *SPACE) + path="${path%SPACE} " + ;; + esac + test_expect_success "reject $pretty at end of path" ' + printf "100644 blob %s\t%s" "$blob" "$path" >tree && + bogus=$(git mktree <tree) && + test_must_fail git read-tree $bogus + ' + + test_expect_success "reject $pretty as subtree" ' + printf "040000 tree %s\t%s" "$tree" "$path" >tree && + bogus=$(git mktree <tree) && + test_must_fail git read-tree $bogus + ' +done <<-EOF +. +.. +.git +.GIT +${u200c}.Git {u200c}.Git +.gI${u200c}T .gI{u200c}T +.GiT${u200c} .GiT{u200c} +git~1 +.git.SPACE .git.{space} +.\\\\.GIT\\\\foobar backslashes +.git\\\\foobar backslashes2 +EOF + +test_expect_success 'utf-8 paths allowed with core.protectHFS off' ' + test_when_finished "git read-tree HEAD" && + test_config core.protectHFS false && + printf "100644 blob %s\t%s" "$blob" ".gi${u200c}t" >tree && + ok=$(git mktree <tree) && + git read-tree $ok +' + +test_done diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh index 8c739c9613..983568a4b9 100755 --- a/t/t1450-fsck.sh +++ b/t/t1450-fsck.sh @@ -251,35 +251,40 @@ test_expect_success 'fsck notices submodule entry pointing to null sha1' ' ) ' -test_expect_success 'fsck notices "." and ".." in trees' ' - ( - git init dots && - cd dots && - blob=$(echo foo | git hash-object -w --stdin) && - tab=$(printf "\\t") && - git mktree <<-EOF && - 100644 blob $blob$tab. - 100644 blob $blob$tab.. - EOF - git fsck 2>out && - cat out && - grep "warning.*\\." out - ) -' - -test_expect_success 'fsck notices ".git" in trees' ' - ( - git init dotgit && - cd dotgit && - blob=$(echo foo | git hash-object -w --stdin) && - tab=$(printf "\\t") && - git mktree <<-EOF && - 100644 blob $blob$tab.git - EOF - git fsck 2>out && - cat out && - grep "warning.*\\.git" out - ) -' +while read name path pretty; do + while read mode type; do + : ${pretty:=$path} + test_expect_success "fsck notices $pretty as $type" ' + ( + git init $name-$type && + cd $name-$type && + echo content >file && + git add file && + git commit -m base && + blob=$(git rev-parse :file) && + tree=$(git rev-parse HEAD^{tree}) && + value=$(eval "echo \$$type") && + printf "$mode $type %s\t%s" "$value" "$path" >bad && + bad_tree=$(git mktree <bad) && + git fsck 2>out && + cat out && + grep "warning.*tree $bad_tree" out + )' + done <<-\EOF + 100644 blob + 040000 tree + EOF +done <<-EOF +dot . +dotdot .. +dotgit .git +dotgit-case .GIT +dotgit-unicode .gI${u200c}T .gI{u200c}T +dotgit-case2 .Git +git-tilde1 git~1 +dotgitdot .git. +dot-backslash-case .\\\\.GIT\\\\foobar +dotgit-case-backslash .git\\\\foobar +EOF test_done diff --git a/t/test-lib.sh b/t/test-lib.sh index 3c7cb1d774..afa411e128 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -158,7 +158,11 @@ _z40=0000000000000000000000000000000000000000 LF=' ' -export _x05 _x40 _z40 LF +# UTF-8 ZERO WIDTH NON-JOINER, which HFS+ ignores +# when case-folding filenames +u200c=$(printf '\342\200\214') + +export _x05 _x40 _z40 LF u200c # Each test should start with something like this, after copyright notices: # diff --git a/unpack-trees.c b/unpack-trees.c index 164354dad7..ca7dd0fa5f 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -102,7 +102,7 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts, opts->unpack_rejects[i].strdup_strings = 1; } -static void do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce, +static int do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce, unsigned int set, unsigned int clear) { clear |= CE_HASHED | CE_UNHASHED; @@ -112,8 +112,8 @@ static void do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce, ce->next = NULL; ce->ce_flags = (ce->ce_flags & ~clear) | set; - add_index_entry(&o->result, ce, - ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); + return add_index_entry(&o->result, ce, + ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); } static struct cache_entry *dup_entry(const struct cache_entry *ce) @@ -608,7 +608,9 @@ static int unpack_nondirectories(int n, unsigned long mask, for (i = 0; i < n; i++) if (src[i] && src[i] != o->df_conflict_entry) - do_add_entry(o, src[i], 0, 0); + if (do_add_entry(o, src[i], 0, 0)) + return -1; + return 0; } @@ -627,3 +627,67 @@ int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding) return chrlen; } + +/* + * Pick the next char from the stream, folding as an HFS+ filename comparison + * would. Note that this is _not_ complete by any means. It's just enough + * to make is_hfs_dotgit() work, and should not be used otherwise. + */ +static ucs_char_t next_hfs_char(const char **in) +{ + while (1) { + ucs_char_t out = pick_one_utf8_char(in, NULL); + /* + * check for malformed utf8. Technically this + * gets converted to a percent-sequence, but + * returning 0 is good enough for is_hfs_dotgit + * to realize it cannot be .git + */ + if (!*in) + return 0; + + /* these code points are ignored completely */ + switch (out) { + case 0x200c: /* ZERO WIDTH NON-JOINER */ + case 0x200d: /* ZERO WIDTH JOINER */ + case 0x200e: /* LEFT-TO-RIGHT MARK */ + case 0x200f: /* RIGHT-TO-LEFT MARK */ + case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */ + case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */ + case 0x202c: /* POP DIRECTIONAL FORMATTING */ + case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */ + case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */ + case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */ + case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */ + case 0x206c: /* INHIBIT ARABIC FORM SHAPING */ + case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */ + case 0x206e: /* NATIONAL DIGIT SHAPES */ + case 0x206f: /* NOMINAL DIGIT SHAPES */ + case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */ + continue; + } + + /* + * there's a great deal of other case-folding that occurs, + * but this is enough to catch anything that will convert + * to ".git" + */ + return tolower(out); + } +} + +int is_hfs_dotgit(const char *path) +{ + ucs_char_t c; + + if (next_hfs_char(&path) != '.' || + next_hfs_char(&path) != 'g' || + next_hfs_char(&path) != 'i' || + next_hfs_char(&path) != 't') + return 0; + c = next_hfs_char(&path); + if (c && !is_dir_sep(c)) + return 0; + + return 1; +} @@ -42,4 +42,12 @@ static inline char *reencode_string(const char *in, int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding); +/* + * Returns true if the the path would match ".git" after HFS case-folding. + * The path should be NUL-terminated, but we will match variants of both ".git\0" + * and ".git/..." (but _not_ ".../.git"). This makes it suitable for both fsck + * and verify_path(). + */ +int is_hfs_dotgit(const char *path); + #endif |