Imported Upstream version 2.15.4 upstream/2.15.4
authorDongHun Kwak <dh0128.kwak@samsung.com>
Wed, 3 Mar 2021 06:16:24 +0000 (15:16 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Wed, 3 Mar 2021 06:16:24 +0000 (15:16 +0900)
37 files changed:
Documentation/RelNotes/2.14.6.txt [new file with mode: 0644]
Documentation/RelNotes/2.15.4.txt [new file with mode: 0644]
Documentation/git-fast-import.txt
Documentation/gitmodules.txt
GIT-VERSION-GEN
RelNotes
builtin/clone.c
builtin/submodule--helper.c
compat/mingw.c
compat/mingw.h
config.mak.uname
connect.c
environment.c
fast-import.c
fsck.c
git-compat-util.h
git-submodule.sh
path.c
read-cache.c
submodule-config.c
submodule.c
submodule.h
t/helper/test-path-utils.c
t/helper/test-run-command.c
t/t0060-path-utils.sh
t/t1014-read-tree-confusing.sh
t/t1450-fsck.sh
t/t6130-pathspec-noglob.sh
t/t7406-submodule-update.sh
t/t7415-submodule-names.sh
t/t7416-submodule-dash-url.sh
t/t7417-submodule-path-url.sh
t/t9300-fast-import.sh
t/t9350-fast-export.sh
transport-helper.c
tree-walk.c
unpack-trees.c

diff --git a/Documentation/RelNotes/2.14.6.txt b/Documentation/RelNotes/2.14.6.txt
new file mode 100644 (file)
index 0000000..72b7af6
--- /dev/null
@@ -0,0 +1,54 @@
+Git v2.14.6 Release Notes
+=========================
+
+This release addresses the security issues CVE-2019-1348,
+CVE-2019-1349, CVE-2019-1350, CVE-2019-1351, CVE-2019-1352,
+CVE-2019-1353, CVE-2019-1354, and CVE-2019-1387.
+
+Fixes since v2.14.5
+-------------------
+
+ * CVE-2019-1348:
+   The --export-marks option of git fast-import is exposed also via
+   the in-stream command feature export-marks=... and it allows
+   overwriting arbitrary paths.
+
+ * CVE-2019-1349:
+   When submodules are cloned recursively, under certain circumstances
+   Git could be fooled into using the same Git directory twice. We now
+   require the directory to be empty.
+
+ * CVE-2019-1350:
+   Incorrect quoting of command-line arguments allowed remote code
+   execution during a recursive clone in conjunction with SSH URLs.
+
+ * CVE-2019-1351:
+   While the only permitted drive letters for physical drives on
+   Windows are letters of the US-English alphabet, this restriction
+   does not apply to virtual drives assigned via subst <letter>:
+   <path>. Git mistook such paths for relative paths, allowing writing
+   outside of the worktree while cloning.
+
+ * CVE-2019-1352:
+   Git was unaware of NTFS Alternate Data Streams, allowing files
+   inside the .git/ directory to be overwritten during a clone.
+
+ * CVE-2019-1353:
+   When running Git in the Windows Subsystem for Linux (also known as
+   "WSL") while accessing a working directory on a regular Windows
+   drive, none of the NTFS protections were active.
+
+ * CVE-2019-1354:
+   Filenames on Linux/Unix can contain backslashes. On Windows,
+   backslashes are directory separators. Git did not use to refuse to
+   write out tracked files with such filenames.
+
+ * CVE-2019-1387:
+   Recursive clones are currently affected by a vulnerability that is
+   caused by too-lax validation of submodule names, allowing very
+   targeted attacks via remote code execution in recursive clones.
+
+Credit for finding these vulnerabilities goes to Microsoft Security
+Response Center, in particular to Nicolas Joly. The `fast-import`
+fixes were provided by Jeff King, the other fixes by Johannes
+Schindelin with help from Garima Singh.
diff --git a/Documentation/RelNotes/2.15.4.txt b/Documentation/RelNotes/2.15.4.txt
new file mode 100644 (file)
index 0000000..dc241cb
--- /dev/null
@@ -0,0 +1,11 @@
+Git v2.15.4 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.14.6 to address
+the security issues CVE-2019-1348, CVE-2019-1349, CVE-2019-1350,
+CVE-2019-1351, CVE-2019-1352, CVE-2019-1353, CVE-2019-1354, and
+CVE-2019-1387; see the release notes for that version for details.
+
+In conjunction with a vulnerability that was fixed in v2.20.2,
+`.gitmodules` is no longer allowed to contain entries of the form
+`submodule.<name>.update=!command`.
index 3d3d219..ff71fc2 100644 (file)
@@ -50,6 +50,21 @@ OPTIONS
        memory used by fast-import during this run.  Showing this output
        is currently the default, but can be disabled with --quiet.
 
+--allow-unsafe-features::
+       Many command-line options can be provided as part of the
+       fast-import stream itself by using the `feature` or `option`
+       commands. However, some of these options are unsafe (e.g.,
+       allowing fast-import to access the filesystem outside of the
+       repository). These options are disabled by default, but can be
+       allowed by providing this option on the command line.  This
+       currently impacts only the `export-marks`, `import-marks`, and
+       `import-marks-if-exists` feature commands.
++
+       Only enable this option if you trust the program generating the
+       fast-import stream! This option is enabled automatically for
+       remote-helpers that use the `import` capability, as they are
+       already trusted to run their own code.
+
 Options for Frontends
 ~~~~~~~~~~~~~~~~~~~~~
 
index db5d47e..ac44a15 100644 (file)
@@ -44,9 +44,8 @@ submodule.<name>.update::
        submodule init` to initialize the configuration variable of
        the same name. Allowed values here are 'checkout', 'rebase',
        'merge' or 'none'. See description of 'update' command in
-       linkgit:git-submodule[1] for their meaning. Note that the
-       '!command' form is intentionally ignored here for security
-       reasons.
+       linkgit:git-submodule[1] for their meaning. For security
+       reasons, the '!command' form is not accepted here.
 
 submodule.<name>.branch::
        A remote branch name for tracking updates in the upstream submodule.
index 4a63ce3..6fe1c9d 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 GVF=GIT-VERSION-FILE
-DEF_VER=v2.15.3
+DEF_VER=v2.15.4
 
 LF='
 '
index e7fe59f..03f4050 120000 (symlink)
--- a/RelNotes
+++ b/RelNotes
@@ -1 +1 @@
-Documentation/RelNotes/2.15.3.txt
\ No newline at end of file
+Documentation/RelNotes/2.15.4.txt
\ No newline at end of file
index dbddd98..73d1c2b 100644 (file)
@@ -758,7 +758,7 @@ static int checkout(int submodule_progress)
 
        if (!err && (option_recurse_submodules.nr > 0)) {
                struct argv_array args = ARGV_ARRAY_INIT;
-               argv_array_pushl(&args, "submodule", "update", "--init", "--recursive", NULL);
+               argv_array_pushl(&args, "submodule", "update", "--require-init", "--recursive", NULL);
 
                if (option_shallow_submodules == 1)
                        argv_array_push(&args, "--depth=1");
index 30e0bb8..0d03fa8 100644 (file)
@@ -13,6 +13,7 @@
 #include "remote.h"
 #include "refs.h"
 #include "connect.h"
+#include "dir.h"
 
 static char *get_default_remote(void)
 {
@@ -616,6 +617,7 @@ static int module_clone(int argc, const char **argv, const char *prefix)
        char *p, *path = NULL, *sm_gitdir;
        struct strbuf sb = STRBUF_INIT;
        struct string_list reference = STRING_LIST_INIT_NODUP;
+       int require_init = 0;
        char *sm_alternate = NULL, *error_strategy = NULL;
 
        struct option module_clone_options[] = {
@@ -640,6 +642,8 @@ static int module_clone(int argc, const char **argv, const char *prefix)
                OPT__QUIET(&quiet, "Suppress output for cloning a submodule"),
                OPT_BOOL(0, "progress", &progress,
                           N_("force cloning progress")),
+               OPT_BOOL(0, "require-init", &require_init,
+                          N_("disallow cloning into non-empty directory")),
                OPT_END()
        };
 
@@ -667,6 +671,10 @@ static int module_clone(int argc, const char **argv, const char *prefix)
        } else
                path = xstrdup(path);
 
+       if (validate_submodule_git_dir(sm_gitdir, name) < 0)
+               die(_("refusing to create/use '%s' in another submodule's "
+                       "git dir"), sm_gitdir);
+
        if (!file_exists(sm_gitdir)) {
                if (safe_create_leading_directories_const(sm_gitdir) < 0)
                        die(_("could not create directory '%s'"), sm_gitdir);
@@ -678,6 +686,8 @@ static int module_clone(int argc, const char **argv, const char *prefix)
                        die(_("clone of '%s' into submodule path '%s' failed"),
                            url, path);
        } else {
+               if (require_init && !access(path, X_OK) && !is_empty_dir(path))
+                       die(_("directory not empty: '%s'"), path);
                if (safe_create_leading_directories_const(path) < 0)
                        die(_("could not create directory '%s'"), path);
                strbuf_addf(&sb, "%s/index", sm_gitdir);
@@ -726,6 +736,7 @@ struct submodule_update_clone {
        int quiet;
        int recommend_shallow;
        struct string_list references;
+       unsigned require_init;
        const char *depth;
        const char *recursive_prefix;
        const char *prefix;
@@ -741,7 +752,7 @@ struct submodule_update_clone {
        int failed_clones_nr, failed_clones_alloc;
 };
 #define SUBMODULE_UPDATE_CLONE_INIT {0, MODULE_LIST_INIT, 0, \
-       SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, \
+       SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, 0, \
        NULL, NULL, NULL, \
        STRING_LIST_INIT_DUP, 0, NULL, 0, 0}
 
@@ -860,6 +871,8 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
                argv_array_pushl(&child->args, "--prefix", suc->prefix, NULL);
        if (suc->recommend_shallow && sub->recommend_shallow == 1)
                argv_array_push(&child->args, "--depth=1");
+       if (suc->require_init)
+               argv_array_push(&child->args, "--require-init");
        argv_array_pushl(&child->args, "--path", sub->path, NULL);
        argv_array_pushl(&child->args, "--name", sub->name, NULL);
        argv_array_pushl(&child->args, "--url", url, NULL);
@@ -1011,6 +1024,8 @@ static int update_clone(int argc, const char **argv, const char *prefix)
                OPT__QUIET(&suc.quiet, N_("don't print cloning progress")),
                OPT_BOOL(0, "progress", &suc.progress,
                            N_("force cloning progress")),
+               OPT_BOOL(0, "require-init", &suc.require_init,
+                          N_("disallow cloning into non-empty directory")),
                OPT_END()
        };
 
index 2d44d21..725cd6c 100644 (file)
@@ -333,6 +333,12 @@ int mingw_mkdir(const char *path, int mode)
 {
        int ret;
        wchar_t wpath[MAX_PATH];
+
+       if (!is_valid_win32_path(path)) {
+               errno = EINVAL;
+               return -1;
+       }
+
        if (xutftowcs_path(wpath, path) < 0)
                return -1;
        ret = _wmkdir(wpath);
@@ -345,13 +351,18 @@ int mingw_open (const char *filename, int oflags, ...)
 {
        va_list args;
        unsigned mode;
-       int fd;
+       int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
        wchar_t wfilename[MAX_PATH];
 
        va_start(args, oflags);
        mode = va_arg(args, int);
        va_end(args);
 
+       if (!is_valid_win32_path(filename)) {
+               errno = create ? EINVAL : ENOENT;
+               return -1;
+       }
+
        if (filename && !strcmp(filename, "/dev/null"))
                filename = "nul";
 
@@ -413,6 +424,11 @@ FILE *mingw_fopen (const char *filename, const char *otype)
        int hide = needs_hiding(filename);
        FILE *file;
        wchar_t wfilename[MAX_PATH], wotype[4];
+       if (!is_valid_win32_path(filename)) {
+               int create = otype && strchr(otype, 'w');
+               errno = create ? EINVAL : ENOENT;
+               return NULL;
+       }
        if (filename && !strcmp(filename, "/dev/null"))
                filename = "nul";
        if (xutftowcs_path(wfilename, filename) < 0 ||
@@ -435,6 +451,11 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
        int hide = needs_hiding(filename);
        FILE *file;
        wchar_t wfilename[MAX_PATH], wotype[4];
+       if (!is_valid_win32_path(filename)) {
+               int create = otype && strchr(otype, 'w');
+               errno = create ? EINVAL : ENOENT;
+               return NULL;
+       }
        if (filename && !strcmp(filename, "/dev/null"))
                filename = "nul";
        if (xutftowcs_path(wfilename, filename) < 0 ||
@@ -872,7 +893,7 @@ static const char *quote_arg(const char *arg)
                                p++;
                                len++;
                        }
-                       if (*p == '"')
+                       if (*p == '"' || !*p)
                                n += count*2 + 1;
                        continue;
                }
@@ -894,16 +915,19 @@ static const char *quote_arg(const char *arg)
                                count++;
                                *d++ = *arg++;
                        }
-                       if (*arg == '"') {
+                       if (*arg == '"' || !*arg) {
                                while (count-- > 0)
                                        *d++ = '\\';
+                               /* don't escape the surrounding end quote */
+                               if (!*arg)
+                                       break;
                                *d++ = '\\';
                        }
                }
                *d++ = *arg++;
        }
        *d++ = '"';
-       *d++ = 0;
+       *d++ = '\0';
        return q;
 }
 
@@ -1965,6 +1989,30 @@ pid_t waitpid(pid_t pid, int *status, int options)
        return -1;
 }
 
+int mingw_has_dos_drive_prefix(const char *path)
+{
+       int i;
+
+       /*
+        * Does it start with an ASCII letter (i.e. highest bit not set),
+        * followed by a colon?
+        */
+       if (!(0x80 & (unsigned char)*path))
+               return *path && path[1] == ':' ? 2 : 0;
+
+       /*
+        * While drive letters must be letters of the English alphabet, it is
+        * possible to assign virtually _any_ Unicode character via `subst` as
+        * a drive letter to "virtual drives". Even `1`, or `ä`. Or fun stuff
+        * like this:
+        *
+        *      subst ֍: %USERPROFILE%\Desktop
+        */
+       for (i = 1; i < 4 && (0x80 & (unsigned char)path[i]); i++)
+               ; /* skip first UTF-8 character */
+       return path[i] == ':' ? i + 1 : 0;
+}
+
 int mingw_skip_dos_drive_prefix(char **path)
 {
        int ret = has_dos_drive_prefix(*path);
@@ -2106,6 +2154,50 @@ static void setup_windows_environment(void)
                setenv("TERM", "cygwin", 1);
 }
 
+int is_valid_win32_path(const char *path)
+{
+       int preceding_space_or_period = 0, i = 0, periods = 0;
+
+       if (!protect_ntfs)
+               return 1;
+
+       skip_dos_drive_prefix((char **)&path);
+
+       for (;;) {
+               char c = *(path++);
+               switch (c) {
+               case '\0':
+               case '/': case '\\':
+                       /* cannot end in ` ` or `.`, except for `.` and `..` */
+                       if (preceding_space_or_period &&
+                           (i != periods || periods > 2))
+                               return 0;
+                       if (!c)
+                               return 1;
+
+                       i = periods = preceding_space_or_period = 0;
+                       continue;
+               case '.':
+                       periods++;
+                       /* fallthru */
+               case ' ':
+                       preceding_space_or_period = 1;
+                       i++;
+                       continue;
+               case ':': /* DOS drive prefix was already skipped */
+               case '<': case '>': case '"': case '|': case '?': case '*':
+                       /* illegal character */
+                       return 0;
+               default:
+                       if (c > '\0' && c < '\x20')
+                               /* illegal character */
+                               return 0;
+               }
+               preceding_space_or_period = 0;
+               i++;
+       }
+}
+
 /*
  * Disable MSVCRT command line wildcard expansion (__getmainargs called from
  * mingw startup code, see init.c in mingw runtime).
index e03aecf..1706466 100644 (file)
@@ -394,8 +394,8 @@ HANDLE winansi_get_osfhandle(int fd);
  * git specific compatibility
  */
 
-#define has_dos_drive_prefix(path) \
-       (isalpha(*(path)) && (path)[1] == ':' ? 2 : 0)
+int mingw_has_dos_drive_prefix(const char *path);
+#define has_dos_drive_prefix mingw_has_dos_drive_prefix
 int mingw_skip_dos_drive_prefix(char **path);
 #define skip_dos_drive_prefix mingw_skip_dos_drive_prefix
 static inline int mingw_is_dir_sep(int c)
@@ -429,6 +429,20 @@ int mingw_offset_1st_component(const char *path);
 #endif
 
 /**
+ * Verifies that the given path is a valid one on Windows.
+ *
+ * In particular, path segments are disallowed which
+ *
+ * - end in a period or a space (except the special directories `.` and `..`).
+ *
+ * - contain any of the reserved characters, e.g. `:`, `;`, `*`, etc
+ *
+ * Returns 1 upon success, otherwise 0.
+ */
+int is_valid_win32_path(const char *path);
+#define is_valid_path(path) is_valid_win32_path(path)
+
+/**
  * Converts UTF-8 encoded string to UTF-16LE.
  *
  * To support repositories with legacy-encoded file names, invalid UTF-8 bytes
index 685a80d..d39a436 100644 (file)
@@ -381,7 +381,6 @@ ifeq ($(uname_S),Windows)
        EXTLIBS = user32.lib advapi32.lib shell32.lib wininet.lib ws2_32.lib invalidcontinue.obj
        PTHREAD_LIBS =
        lib =
-       BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1
 ifndef DEBUG
        BASIC_CFLAGS += -GL -Os -MD
        BASIC_LDFLAGS += -LTCG
@@ -519,7 +518,6 @@ 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
        EXTLIBS += -lws2_32
        GITLIBS += git.res
        PTHREAD_LIBS =
index df56c0c..15eeb4a 100644 (file)
--- a/connect.c
+++ b/connect.c
@@ -264,7 +264,7 @@ int url_is_local_not_ssh(const char *url)
        const char *colon = strchr(url, ':');
        const char *slash = strchr(url, '/');
        return !colon || (slash && slash < colon) ||
-               has_dos_drive_prefix(url);
+               (has_dos_drive_prefix(url) && is_valid_path(url));
 }
 
 static const char *prot_name(enum protocol protocol)
index 8289c25..bca1187 100644 (file)
@@ -73,7 +73,7 @@ enum log_refs_config log_all_ref_updates = LOG_REFS_UNSET;
 int protect_hfs = PROTECT_HFS_DEFAULT;
 
 #ifndef PROTECT_NTFS_DEFAULT
-#define PROTECT_NTFS_DEFAULT 0
+#define PROTECT_NTFS_DEFAULT 1
 #endif
 int protect_ntfs = PROTECT_NTFS_DEFAULT;
 
index d5e4cf0..b7167ab 100644 (file)
@@ -367,6 +367,7 @@ static uintmax_t next_mark;
 static struct strbuf new_data = STRBUF_INIT;
 static int seen_data_command;
 static int require_explicit_termination;
+static int allow_unsafe_features;
 
 /* Signal handling */
 static volatile sig_atomic_t checkpoint_requested;
@@ -1862,6 +1863,12 @@ static void dump_marks(void)
        if (!export_marks_file || (import_marks_file && !import_marks_file_done))
                return;
 
+       if (safe_create_leading_directories_const(export_marks_file)) {
+               failure |= error_errno("unable to create leading directories of %s",
+                                      export_marks_file);
+               return;
+       }
+
        if (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) {
                failure |= error_errno("Unable to write marks file %s",
                                       export_marks_file);
@@ -3229,7 +3236,6 @@ static void option_import_marks(const char *marks,
        }
 
        import_marks_file = make_fast_import_path(marks);
-       safe_create_leading_directories_const(import_marks_file);
        import_marks_file_from_stream = from_stream;
        import_marks_file_ignore_missing = ignore_missing;
 }
@@ -3270,7 +3276,6 @@ static void option_active_branches(const char *branches)
 static void option_export_marks(const char *marks)
 {
        export_marks_file = make_fast_import_path(marks);
-       safe_create_leading_directories_const(export_marks_file);
 }
 
 static void option_cat_blob_fd(const char *fd)
@@ -3313,10 +3318,12 @@ static int parse_one_option(const char *option)
                option_active_branches(option);
        } else if (skip_prefix(option, "export-pack-edges=", &option)) {
                option_export_pack_edges(option);
-       } else if (starts_with(option, "quiet")) {
+       } else if (!strcmp(option, "quiet")) {
                show_stats = 0;
-       } else if (starts_with(option, "stats")) {
+       } else if (!strcmp(option, "stats")) {
                show_stats = 1;
+       } else if (!strcmp(option, "allow-unsafe-features")) {
+               ; /* already handled during early option parsing */
        } else {
                return 0;
        }
@@ -3324,6 +3331,13 @@ static int parse_one_option(const char *option)
        return 1;
 }
 
+static void check_unsafe_feature(const char *feature, int from_stream)
+{
+       if (from_stream && !allow_unsafe_features)
+               die(_("feature '%s' forbidden in input without --allow-unsafe-features"),
+                   feature);
+}
+
 static int parse_one_feature(const char *feature, int from_stream)
 {
        const char *arg;
@@ -3331,10 +3345,13 @@ static int parse_one_feature(const char *feature, int from_stream)
        if (skip_prefix(feature, "date-format=", &arg)) {
                option_date_format(arg);
        } else if (skip_prefix(feature, "import-marks=", &arg)) {
+               check_unsafe_feature("import-marks", from_stream);
                option_import_marks(arg, from_stream, 0);
        } else if (skip_prefix(feature, "import-marks-if-exists=", &arg)) {
+               check_unsafe_feature("import-marks-if-exists", from_stream);
                option_import_marks(arg, from_stream, 1);
        } else if (skip_prefix(feature, "export-marks=", &arg)) {
+               check_unsafe_feature(feature, from_stream);
                option_export_marks(arg);
        } else if (!strcmp(feature, "get-mark")) {
                ; /* Don't die - this feature is supported */
@@ -3461,6 +3478,20 @@ int cmd_main(int argc, const char **argv)
        avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*));
        marks = pool_calloc(1, sizeof(struct mark_set));
 
+       /*
+        * We don't parse most options until after we've seen the set of
+        * "feature" lines at the start of the stream (which allows the command
+        * line to override stream data). But we must do an early parse of any
+        * command-line options that impact how we interpret the feature lines.
+        */
+       for (i = 1; i < argc; i++) {
+               const char *arg = argv[i];
+               if (*arg != '-' || !strcmp(arg, "--"))
+                       break;
+               if (!strcmp(arg, "--allow-unsafe-features"))
+                       allow_unsafe_features = 1;
+       }
+
        global_argc = argc;
        global_argv = argv;
 
diff --git a/fsck.c b/fsck.c
index 032699e..0b2b93a 100644 (file)
--- a/fsck.c
+++ b/fsck.c
@@ -551,7 +551,7 @@ static int fsck_tree(struct tree *item, struct fsck_options *options)
 
        while (desc.size) {
                unsigned mode;
-               const char *name;
+               const char *name, *backslash;
                const struct object_id *oid;
 
                oid = tree_entry_extract(&desc, &name, &mode);
@@ -565,6 +565,15 @@ static int fsck_tree(struct tree *item, struct fsck_options *options)
                               is_hfs_dotgit(name) ||
                               is_ntfs_dotgit(name));
                has_zero_pad |= *(char *)desc.buffer == '0';
+
+               if ((backslash = strchr(name, '\\'))) {
+                       while (backslash) {
+                               backslash++;
+                               has_dotgit |= is_ntfs_dotgit(backslash);
+                               backslash = strchr(backslash, '\\');
+                       }
+               }
+
                if (update_tree_entry_gently(&desc)) {
                        retval += report(options, &item->object, FSCK_MSG_BAD_TREE, "cannot be parsed as a tree");
                        break;
index 0d270ff..be8e429 100644 (file)
@@ -370,6 +370,10 @@ static inline int git_offset_1st_component(const char *path)
 #define offset_1st_component git_offset_1st_component
 #endif
 
+#ifndef is_valid_path
+#define is_valid_path(path) 1
+#endif
+
 #ifndef find_last_dir_sep
 static inline char *git_find_last_dir_sep(const char *path)
 {
index 8f260fb..e4843a5 100755 (executable)
@@ -34,6 +34,7 @@ reference=
 cached=
 recursive=
 init=
+require_init=
 files=
 remote=
 nofetch=
@@ -528,6 +529,10 @@ cmd_update()
                -i|--init)
                        init=1
                        ;;
+               --require-init)
+                       init=1
+                       require_init=1
+                       ;;
                --remote)
                        remote=1
                        ;;
@@ -606,6 +611,7 @@ cmd_update()
                ${update:+--update "$update"} \
                ${reference:+"$reference"} \
                ${depth:+--depth "$depth"} \
+               ${require_init:+--require-init} \
                ${recommend_shallow:+"$recommend_shallow"} \
                ${jobs:+$jobs} \
                "$@" || echo "#unmatched" $?
diff --git a/path.c b/path.c
index 4c4a751..84e8824 100644 (file)
--- a/path.c
+++ b/path.c
@@ -1289,37 +1289,77 @@ int daemon_avoid_alias(const char *p)
        }
 }
 
-static int only_spaces_and_periods(const char *path, size_t len, size_t skip)
+/*
+ * On NTFS, we need to be careful to disallow certain synonyms of the `.git/`
+ * directory:
+ *
+ * - For historical reasons, file names that end in spaces or periods are
+ *   automatically trimmed. Therefore, `.git . . ./` is a valid way to refer
+ *   to `.git/`.
+ *
+ * - For other historical reasons, file names that do not conform to the 8.3
+ *   format (up to eight characters for the basename, three for the file
+ *   extension, certain characters not allowed such as `+`, etc) are associated
+ *   with a so-called "short name", at least on the `C:` drive by default.
+ *   Which means that `git~1/` is a valid way to refer to `.git/`.
+ *
+ *   Note: Technically, `.git/` could receive the short name `git~2` if the
+ *   short name `git~1` were already used. In Git, however, we guarantee that
+ *   `.git` is the first item in a directory, therefore it will be associated
+ *   with the short name `git~1` (unless short names are disabled).
+ *
+ * - For yet other historical reasons, NTFS supports so-called "Alternate Data
+ *   Streams", i.e. metadata associated with a given file, referred to via
+ *   `<filename>:<stream-name>:<stream-type>`. There exists a default stream
+ *   type for directories, allowing `.git/` to be accessed via
+ *   `.git::$INDEX_ALLOCATION/`.
+ *
+ * When this function returns 1, it indicates that the specified file/directory
+ * name refers to a `.git` file or directory, or to any of these synonyms, and
+ * Git should therefore not track it.
+ *
+ * For performance reasons, _all_ Alternate Data Streams of `.git/` are
+ * forbidden, not just `::$INDEX_ALLOCATION`.
+ *
+ * This function is intended to be used by `git fsck` even on platforms where
+ * the backslash is a regular filename character, therefore it needs to handle
+ * backlash characters in the provided `name` specially: they are interpreted
+ * as directory separators.
+ */
+int is_ntfs_dotgit(const char *name)
 {
-       if (len < skip)
+       char c;
+
+       /*
+        * Note that when we don't find `.git` or `git~1` we end up with `name`
+        * advanced partway through the string. That's okay, though, as we
+        * return immediately in those cases, without looking at `name` any
+        * further.
+        */
+       c = *(name++);
+       if (c == '.') {
+               /* .git */
+               if (((c = *(name++)) != 'g' && c != 'G') ||
+                   ((c = *(name++)) != 'i' && c != 'I') ||
+                   ((c = *(name++)) != 't' && c != 'T'))
+                       return 0;
+       } else if (c == 'g' || c == 'G') {
+               /* git ~1 */
+               if (((c = *(name++)) != 'i' && c != 'I') ||
+                   ((c = *(name++)) != 't' && c != 'T') ||
+                   *(name++) != '~' ||
+                   *(name++) != '1')
+                       return 0;
+       } else
                return 0;
-       len -= skip;
-       path += skip;
-       while (len-- > 0) {
-               char c = *(path++);
-               if (c != ' ' && c != '.')
+
+       for (;;) {
+               c = *(name++);
+               if (!c || c == '\\' || c == '/' || c == ':')
+                       return 1;
+               if (c != '.' && c != ' ')
                        return 0;
        }
-       return 1;
-}
-
-int is_ntfs_dotgit(const char *name)
-{
-       size_t 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;
-               }
 }
 
 static int is_ntfs_dot_generic(const char *name,
@@ -1335,7 +1375,7 @@ static int is_ntfs_dot_generic(const char *name,
 only_spaces_and_periods:
                for (;;) {
                        char c = name[i++];
-                       if (!c)
+                       if (!c || c == ':')
                                return 1;
                        if (c != ' ' && c != '.')
                                return 0;
index 8cef7b6..2a280b8 100644 (file)
@@ -848,6 +848,9 @@ int verify_path(const char *path, unsigned mode)
        if (has_dos_drive_prefix(path))
                return 0;
 
+       if (!is_valid_path(path))
+               return 0;
+
        goto inside;
        for (;;) {
                if (!c)
@@ -875,7 +878,15 @@ inside:
                        if ((c == '.' && !verify_dotfile(path, mode)) ||
                            is_dir_sep(c) || c == '\0')
                                return 0;
+               } else if (c == '\\' && protect_ntfs) {
+                       if (is_ntfs_dotgit(path))
+                               return 0;
+                       if (S_ISLNK(mode)) {
+                               if (is_ntfs_dotgitmodules(path))
+                                       return 0;
+                       }
                }
+
                c = *path++;
        }
 }
index 3414fa1..464908d 100644 (file)
@@ -396,6 +396,13 @@ struct parse_config_parameter {
        int overwrite;
 };
 
+/*
+ * Parse a config item from .gitmodules.
+ *
+ * This does not handle submodule-related configuration from the main
+ * config store (.git/config, etc).  Callers are responsible for
+ * checking for overrides in the main config store when appropriate.
+ */
 static int parse_config(const char *var, const char *value, void *data)
 {
        struct parse_config_parameter *me = data;
@@ -473,8 +480,9 @@ static int parse_config(const char *var, const char *value, void *data)
                        warn_multiple_config(me->treeish_name, submodule->name,
                                             "update");
                else if (parse_submodule_update_strategy(value,
-                        &submodule->update_strategy) < 0)
-                               die(_("invalid value for %s"), var);
+                        &submodule->update_strategy) < 0 ||
+                        submodule->update_strategy.type == SM_UPDATE_COMMAND)
+                       die(_("invalid value for %s"), var);
        } else if (!strcmp(item.buf, "shallow")) {
                if (!me->overwrite && submodule->recommend_shallow != -1)
                        warn_multiple_config(me->treeish_name, submodule->name,
index 63e7094..2ae209f 100644 (file)
@@ -1794,6 +1794,47 @@ int merge_submodule(struct object_id *result, const char *path,
        return 0;
 }
 
+int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
+{
+       size_t len = strlen(git_dir), suffix_len = strlen(submodule_name);
+       char *p;
+       int ret = 0;
+
+       if (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||
+           strcmp(p, submodule_name))
+               BUG("submodule name '%s' not a suffix of git dir '%s'",
+                   submodule_name, git_dir);
+
+       /*
+        * We prevent the contents of sibling submodules' git directories to
+        * clash.
+        *
+        * Example: having a submodule named `hippo` and another one named
+        * `hippo/hooks` would result in the git directories
+        * `.git/modules/hippo/` and `.git/modules/hippo/hooks/`, respectively,
+        * but the latter directory is already designated to contain the hooks
+        * of the former.
+        */
+       for (; *p; p++) {
+               if (is_dir_sep(*p)) {
+                       char c = *p;
+
+                       *p = '\0';
+                       if (is_git_directory(git_dir))
+                               ret = -1;
+                       *p = c;
+
+                       if (ret < 0)
+                               return error(_("submodule git dir '%s' is "
+                                              "inside git dir '%.*s'"),
+                                            git_dir,
+                                            (int)(p - git_dir), git_dir);
+               }
+       }
+
+       return 0;
+}
+
 /*
  * Embeds a single submodules git directory into the superprojects git dir,
  * non recursively.
@@ -1802,7 +1843,7 @@ static void relocate_single_git_dir_into_superproject(const char *prefix,
                                                      const char *path)
 {
        char *old_git_dir = NULL, *real_old_git_dir = NULL, *real_new_git_dir = NULL;
-       const char *new_git_dir;
+       char *new_git_dir;
        const struct submodule *sub;
 
        if (submodule_uses_worktrees(path))
@@ -1820,10 +1861,14 @@ static void relocate_single_git_dir_into_superproject(const char *prefix,
        if (!sub)
                die(_("could not lookup name for submodule '%s'"), path);
 
-       new_git_dir = git_path("modules/%s", sub->name);
+       new_git_dir = git_pathdup("modules/%s", sub->name);
+       if (validate_submodule_git_dir(new_git_dir, sub->name) < 0)
+               die(_("refusing to move '%s' into an existing git dir"),
+                   real_old_git_dir);
        if (safe_create_leading_directories_const(new_git_dir) < 0)
                die(_("could not create directory '%s'"), new_git_dir);
        real_new_git_dir = real_pathdup(new_git_dir, 1);
+       free(new_git_dir);
 
        fprintf(stderr, _("Migrating git directory of '%s%s' from\n'%s' to\n'%s'\n"),
                get_super_prefix_or_empty(), path,
index f0da027..2504016 100644 (file)
@@ -111,6 +111,11 @@ extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git
  */
 int submodule_to_gitdir(struct strbuf *buf, const char *submodule);
 
+/*
+ * Make sure that no submodule's git dir is nested in a sibling submodule's.
+ */
+int validate_submodule_git_dir(char *git_dir, const char *submodule_name);
+
 #define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0)
 #define SUBMODULE_MOVE_HEAD_FORCE   (1<<1)
 extern int submodule_move_head(const char *path,
index 9484655..8b3ce07 100644 (file)
@@ -176,6 +176,99 @@ static int is_dotgitmodules(const char *path)
        return is_hfs_dotgitmodules(path) || is_ntfs_dotgitmodules(path);
 }
 
+/*
+ * A very simple, reproducible pseudo-random generator. Copied from
+ * `test-genrandom.c`.
+ */
+static uint64_t my_random_value = 1234;
+
+static uint64_t my_random(void)
+{
+       my_random_value = my_random_value * 1103515245 + 12345;
+       return my_random_value;
+}
+
+/*
+ * A fast approximation of the square root, without requiring math.h.
+ *
+ * It uses Newton's method to approximate the solution of 0 = x^2 - value.
+ */
+static double my_sqrt(double value)
+{
+       const double epsilon = 1e-6;
+       double x = value;
+
+       if (value == 0)
+               return 0;
+
+       for (;;) {
+               double delta = (value / x - x) / 2;
+               if (delta < epsilon && delta > -epsilon)
+                       return x + delta;
+               x += delta;
+       }
+}
+
+static int protect_ntfs_hfs_benchmark(int argc, const char **argv)
+{
+       size_t i, j, nr, min_len = 3, max_len = 20;
+       char **names;
+       int repetitions = 15, file_mode = 0100644;
+       uint64_t begin, end;
+       double m[3][2], v[3][2];
+       uint64_t cumul;
+       double cumul2;
+
+       if (argc > 1 && !strcmp(argv[1], "--with-symlink-mode")) {
+               file_mode = 0120000;
+               argc--;
+               argv++;
+       }
+
+       nr = argc > 1 ? strtoul(argv[1], NULL, 0) : 1000000;
+       ALLOC_ARRAY(names, nr);
+
+       if (argc > 2) {
+               min_len = strtoul(argv[2], NULL, 0);
+               if (argc > 3)
+                       max_len = strtoul(argv[3], NULL, 0);
+               if (min_len > max_len)
+                       die("min_len > max_len");
+       }
+
+       for (i = 0; i < nr; i++) {
+               size_t len = min_len + (my_random() % (max_len + 1 - min_len));
+
+               names[i] = xmallocz(len);
+               while (len > 0)
+                       names[i][--len] = (char)(' ' + (my_random() % ('\x7f' - ' ')));
+       }
+
+       for (protect_ntfs = 0; protect_ntfs < 2; protect_ntfs++)
+               for (protect_hfs = 0; protect_hfs < 2; protect_hfs++) {
+                       cumul = 0;
+                       cumul2 = 0;
+                       for (i = 0; i < repetitions; i++) {
+                               begin = getnanotime();
+                               for (j = 0; j < nr; j++)
+                                       verify_path(names[j], file_mode);
+                               end = getnanotime();
+                               printf("protect_ntfs = %d, protect_hfs = %d: %lfms\n", protect_ntfs, protect_hfs, (end-begin) / (double)1e6);
+                               cumul += end - begin;
+                               cumul2 += (end - begin) * (end - begin);
+                       }
+                       m[protect_ntfs][protect_hfs] = cumul / (double)repetitions;
+                       v[protect_ntfs][protect_hfs] = my_sqrt(cumul2 / (double)repetitions - m[protect_ntfs][protect_hfs] * m[protect_ntfs][protect_hfs]);
+                       printf("mean: %lfms, stddev: %lfms\n", m[protect_ntfs][protect_hfs] / (double)1e6, v[protect_ntfs][protect_hfs] / (double)1e6);
+               }
+
+       for (protect_ntfs = 0; protect_ntfs < 2; protect_ntfs++)
+               for (protect_hfs = 0; protect_hfs < 2; protect_hfs++)
+                       printf("ntfs=%d/hfs=%d: %lf%% slower\n", protect_ntfs, protect_hfs, (m[protect_ntfs][protect_hfs] - m[0][0]) * 100 / m[0][0]);
+
+       return 0;
+}
+
 int cmd_main(int argc, const char **argv)
 {
        if (argc == 3 && !strcmp(argv[1], "normalize_path_copy")) {
@@ -290,6 +383,26 @@ int cmd_main(int argc, const char **argv)
                return !!res;
        }
 
+       if (argc > 1 && !strcmp(argv[1], "protect_ntfs_hfs"))
+               return !!protect_ntfs_hfs_benchmark(argc - 1, argv + 1);
+
+       if (argc > 1 && !strcmp(argv[1], "is_valid_path")) {
+               int res = 0, expect = 1, i;
+
+               for (i = 2; i < argc; i++)
+                       if (!strcmp("--not", argv[i]))
+                               expect = 0;
+                       else if (expect != is_valid_path(argv[i]))
+                               res = error("'%s' is%s a valid path",
+                                           argv[i], expect ? " not" : "");
+                       else
+                               fprintf(stderr,
+                                       "'%s' is%s a valid path\n",
+                                       argv[i], expect ? "" : " not");
+
+               return !!res;
+       }
+
        fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
                argv[1] ? argv[1] : "(there was none)");
        return 1;
index d24d157..b622334 100644 (file)
@@ -12,8 +12,8 @@
 #include "run-command.h"
 #include "argv-array.h"
 #include "strbuf.h"
-#include <string.h>
-#include <errno.h>
+#include "gettext.h"
+#include "parse-options.h"
 
 static int number_callbacks;
 static int parallel_next(struct child_process *cp,
@@ -49,11 +49,145 @@ static int task_finished(int result,
        return 1;
 }
 
+static uint64_t my_random_next = 1234;
+
+static uint64_t my_random(void)
+{
+       uint64_t res = my_random_next;
+       my_random_next = my_random_next * 1103515245 + 12345;
+       return res;
+}
+
+static int quote_stress_test(int argc, const char **argv)
+{
+       /*
+        * We are running a quote-stress test.
+        * spawn a subprocess that runs quote-stress with a
+        * special option that echoes back the arguments that
+        * were passed in.
+        */
+       char special[] = ".?*\\^_\"'`{}()[]<>@~&+:;$%"; // \t\r\n\a";
+       int i, j, k, trials = 100, skip = 0, msys2 = 0;
+       struct strbuf out = STRBUF_INIT;
+       struct argv_array args = ARGV_ARRAY_INIT;
+       struct option options[] = {
+               OPT_INTEGER('n', "trials", &trials, "Number of trials"),
+               OPT_INTEGER('s', "skip", &skip, "Skip <n> trials"),
+               OPT_BOOL('m', "msys2", &msys2, "Test quoting for MSYS2's sh"),
+               OPT_END()
+       };
+       const char * const usage[] = {
+               "test-run-command quote-stress-test <options>",
+               NULL
+       };
+
+       argc = parse_options(argc, argv, NULL, options, usage, 0);
+
+       setenv("MSYS_NO_PATHCONV", "1", 0);
+
+       for (i = 0; i < trials; i++) {
+               struct child_process cp = CHILD_PROCESS_INIT;
+               size_t arg_count, arg_offset;
+               int ret = 0;
+
+               argv_array_clear(&args);
+               if (msys2)
+                       argv_array_pushl(&args, "sh", "-c",
+                                        "printf %s\\\\0 \"$@\"", "skip", NULL);
+               else
+                       argv_array_pushl(&args, "test-run-command",
+                                        "quote-echo", NULL);
+               arg_offset = args.argc;
+
+               if (argc > 0) {
+                       trials = 1;
+                       arg_count = argc;
+                       for (j = 0; j < arg_count; j++)
+                               argv_array_push(&args, argv[j]);
+               } else {
+                       arg_count = 1 + (my_random() % 5);
+                       for (j = 0; j < arg_count; j++) {
+                               char buf[20];
+                               size_t min_len = 1;
+                               size_t arg_len = min_len +
+                                       (my_random() % (ARRAY_SIZE(buf) - min_len));
+
+                               for (k = 0; k < arg_len; k++)
+                                       buf[k] = special[my_random() %
+                                               ARRAY_SIZE(special)];
+                               buf[arg_len] = '\0';
+
+                               argv_array_push(&args, buf);
+                       }
+               }
+
+               if (i < skip)
+                       continue;
+
+               cp.argv = args.argv;
+               strbuf_reset(&out);
+               if (pipe_command(&cp, NULL, 0, &out, 0, NULL, 0) < 0)
+                       return error("Failed to spawn child process");
+
+               for (j = 0, k = 0; j < arg_count; j++) {
+                       const char *arg = args.argv[j + arg_offset];
+
+                       if (strcmp(arg, out.buf + k))
+                               ret = error("incorrectly quoted arg: '%s', "
+                                           "echoed back as '%s'",
+                                            arg, out.buf + k);
+                       k += strlen(out.buf + k) + 1;
+               }
+
+               if (k != out.len)
+                       ret = error("got %d bytes, but consumed only %d",
+                                    (int)out.len, (int)k);
+
+               if (ret) {
+                       fprintf(stderr, "Trial #%d failed. Arguments:\n", i);
+                       for (j = 0; j < arg_count; j++)
+                               fprintf(stderr, "arg #%d: '%s'\n",
+                                       (int)j, args.argv[j + arg_offset]);
+
+                       strbuf_release(&out);
+                       argv_array_clear(&args);
+
+                       return ret;
+               }
+
+               if (i && (i % 100) == 0)
+                       fprintf(stderr, "Trials completed: %d\n", (int)i);
+       }
+
+       strbuf_release(&out);
+       argv_array_clear(&args);
+
+       return 0;
+}
+
+static int quote_echo(int argc, const char **argv)
+{
+       while (argc > 1) {
+               fwrite(argv[1], strlen(argv[1]), 1, stdout);
+               fputc('\0', stdout);
+               argv++;
+               argc--;
+       }
+
+       return 0;
+}
+
 int cmd_main(int argc, const char **argv)
 {
        struct child_process proc = CHILD_PROCESS_INIT;
        int jobs;
 
+       if (argc >= 2 && !strcmp(argv[1], "quote-stress-test"))
+               return !!quote_stress_test(argc - 1, argv + 1);
+
+       if (argc >= 2 && !strcmp(argv[1], "quote-echo"))
+               return !!quote_echo(argc - 1, argv + 1);
+
        if (argc < 3)
                return 1;
        proc.argv = (const char **)argv + 2;
index 3f3357e..40db3e1 100755 (executable)
@@ -165,6 +165,15 @@ test_expect_success 'absolute path rejects the empty string' '
        test_must_fail test-path-utils absolute_path ""
 '
 
+test_expect_success MINGW '<drive-letter>:\\abc is an absolute path' '
+       for letter in : \" C Z 1 ä
+       do
+               path=$letter:\\abc &&
+               absolute="$(test-path-utils absolute_path "$path")" &&
+               test "$path" = "$absolute" || return 1
+       done
+'
+
 test_expect_success 'real path rejects the empty string' '
        test_must_fail test-path-utils real_path ""
 '
@@ -408,6 +417,9 @@ test_expect_success 'match .gitmodules' '
                ~1000000 \
                ~9999999 \
                \
+               .gitmodules:\$DATA \
+               "gitmod~4 . :\$DATA" \
+               \
                --not \
                ".gitmodules x"  \
                ".gitmodules .x" \
@@ -432,7 +444,25 @@ test_expect_success 'match .gitmodules' '
                \
                GI7EB~1 \
                GI7EB~01 \
-               GI7EB~1X
+               GI7EB~1X \
+               \
+               .gitmodules,:\$DATA
+'
+
+test_expect_success MINGW 'is_valid_path() on Windows' '
+       test-path-utils is_valid_path \
+               win32 \
+               "win32 x" \
+               ../hello.txt \
+               C:\\git \
+               \
+               --not \
+               "win32 "  \
+               "win32 /x "  \
+               "win32."  \
+               "win32 . ." \
+               .../hello.txt \
+               colon:test
 '
 
 test_done
index 2f5a25d..da3376b 100755 (executable)
@@ -49,6 +49,7 @@ git~1
 .git.SPACE .git.{space}
 .\\\\.GIT\\\\foobar backslashes
 .git\\\\foobar backslashes2
+.git...:alternate-stream
 EOF
 
 test_expect_success 'utf-8 paths allowed with core.protectHFS off' '
index cb4b66e..33c955f 100755 (executable)
@@ -419,6 +419,7 @@ while read name path pretty; do
                (
                        git init $name-$type &&
                        cd $name-$type &&
+                       git config core.protectNTFS false &&
                        echo content >file &&
                        git add file &&
                        git commit -m base &&
index 6583532..4129d9f 100755 (executable)
@@ -10,6 +10,7 @@ test_expect_success 'create commits with glob characters' '
        # the name "f*" in the worktree, because it is not allowed
        # on Windows (the tests below do not depend on the presence
        # of the file in the worktree)
+       git config core.protectNTFS false &&
        git update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:foo)" "f*" &&
        test_tick &&
        git commit -m star &&
index 6f083c4..7799324 100755 (executable)
@@ -406,12 +406,12 @@ test_expect_success 'submodule update - command in .git/config' '
        )
 '
 
-test_expect_success 'submodule update - command in .gitmodules is ignored' '
+test_expect_success 'submodule update - command in .gitmodules is rejected' '
        test_when_finished "git -C super reset --hard HEAD^" &&
        git -C super config -f .gitmodules submodule.submodule.update "!false" &&
        git -C super commit -a -m "add command to .gitmodules file" &&
        git -C super/submodule reset --hard $submodulesha1^ &&
-       git -C super submodule update submodule
+       test_must_fail git -C super submodule update submodule
 '
 
 cat << EOF >expect
@@ -480,6 +480,9 @@ test_expect_success 'recursive submodule update - command in .git/config catches
 '
 
 test_expect_success 'submodule init does not copy command into .git/config' '
+       test_when_finished "git -C super update-index --force-remove submodule1" &&
+       test_when_finished git config -f super/.gitmodules \
+               --remove-section submodule.submodule1 &&
        (cd super &&
         H=$(git ls-files -s submodule | cut -d" " -f2) &&
         mkdir submodule1 &&
@@ -487,10 +490,9 @@ test_expect_success 'submodule init does not copy command into .git/config' '
         git config -f .gitmodules submodule.submodule1.path submodule1 &&
         git config -f .gitmodules submodule.submodule1.url ../submodule &&
         git config -f .gitmodules submodule.submodule1.update !false &&
-        git submodule init submodule1 &&
-        echo "none" >expect &&
-        git config submodule.submodule1.update >actual &&
-        test_cmp expect actual
+        test_must_fail git submodule init submodule1 &&
+        test_expect_code 1 git config submodule.submodule1.update >actual &&
+        test_must_be_empty actual
        )
 '
 
index 75fa071..0338b5c 100755 (executable)
@@ -73,4 +73,60 @@ test_expect_success 'clone evil superproject' '
        ! grep "RUNNING POST CHECKOUT" output
 '
 
+test_expect_success MINGW 'prevent git~1 squatting on Windows' '
+       git init squatting &&
+       (
+               cd squatting &&
+               mkdir a &&
+               touch a/..git &&
+               git add a/..git &&
+               test_tick &&
+               git commit -m initial &&
+
+               modules="$(test_write_lines \
+                       "[submodule \"b.\"]" "url = ." "path = c" \
+                       "[submodule \"b\"]" "url = ." "path = d\\\\a" |
+                       git hash-object -w --stdin)" &&
+               rev="$(git rev-parse --verify HEAD)" &&
+               hash="$(echo x | git hash-object -w --stdin)" &&
+               git -c core.protectNTFS=false update-index --add \
+                       --cacheinfo 100644,$modules,.gitmodules \
+                       --cacheinfo 160000,$rev,c \
+                       --cacheinfo 160000,$rev,d\\a \
+                       --cacheinfo 100644,$hash,d./a/x \
+                       --cacheinfo 100644,$hash,d./a/..git &&
+               test_tick &&
+               git -c core.protectNTFS=false commit -m "module" &&
+               test_must_fail git show HEAD: 2>err &&
+               test_i18ngrep backslash err
+       ) &&
+       test_must_fail git -c core.protectNTFS=false \
+               clone --recurse-submodules squatting squatting-clone 2>err &&
+       test_i18ngrep -e "directory not empty" -e "not an empty directory" err &&
+       ! grep gitdir squatting-clone/d/a/git~2
+'
+
+test_expect_success 'git dirs of sibling submodules must not be nested' '
+       git init nested &&
+       test_commit -C nested nested &&
+       (
+               cd nested &&
+               cat >.gitmodules <<-EOF &&
+               [submodule "hippo"]
+                       url = .
+                       path = thing1
+               [submodule "hippo/hooks"]
+                       url = .
+                       path = thing2
+               EOF
+               git clone . thing1 &&
+               git clone . thing2 &&
+               git add .gitmodules thing1 thing2 &&
+               test_tick &&
+               git commit -m nested
+       ) &&
+       test_must_fail git clone --recurse-submodules nested clone 2>err &&
+       test_i18ngrep "is inside git dir" err
+'
+
 test_done
index 459193c..2966e93 100755 (executable)
@@ -31,4 +31,18 @@ test_expect_success 'clone rejects unprotected dash' '
        test_i18ngrep ignoring err
 '
 
+test_expect_success 'trailing backslash is handled correctly' '
+       git init testmodule &&
+       test_commit -C testmodule c &&
+       git submodule add ./testmodule &&
+       : ensure that the name ends in a double backslash &&
+       sed -e "s|\\(submodule \"testmodule\\)\"|\\1\\\\\\\\\"|" \
+               -e "s|url = .*|url = \" --should-not-be-an-option\"|" \
+               <.gitmodules >.new &&
+       mv .new .gitmodules &&
+       git commit -am "Add testmodule" &&
+       test_must_fail git clone --verbose --recurse-submodules . dolly 2>err &&
+       test_i18ngrep ! "unknown option" err
+'
+
 test_done
index 638293f..fad9e20 100755 (executable)
@@ -17,4 +17,21 @@ test_expect_success 'clone rejects unprotected dash' '
        test_i18ngrep ignoring err
 '
 
+test_expect_success MINGW 'submodule paths disallows trailing spaces' '
+       git init super &&
+       test_must_fail git -C super submodule add ../upstream "sub " &&
+
+       : add "sub", then rename "sub" to "sub ", the hard way &&
+       git -C super submodule add ../upstream sub &&
+       tree=$(git -C super write-tree) &&
+       git -C super ls-tree $tree >tree &&
+       sed "s/sub/sub /" <tree >tree.new &&
+       tree=$(git -C super mktree <tree.new) &&
+       commit=$(echo with space | git -C super commit-tree $tree) &&
+       git -C super update-ref refs/heads/master $commit &&
+
+       test_must_fail git clone --recurse-submodules super dst 2>err &&
+       test_i18ngrep "sub " err
+'
+
 test_done
index d47560b..77104f9 100755 (executable)
@@ -2106,12 +2106,27 @@ test_expect_success 'R: abort on receiving feature after data command' '
        test_must_fail git fast-import <input
 '
 
+test_expect_success 'R: import-marks features forbidden by default' '
+       >git.marks &&
+       echo "feature import-marks=git.marks" >input &&
+       test_must_fail git fast-import <input &&
+       echo "feature import-marks-if-exists=git.marks" >input &&
+       test_must_fail git fast-import <input
+'
+
 test_expect_success 'R: only one import-marks feature allowed per stream' '
+       >git.marks &&
+       >git2.marks &&
        cat >input <<-EOF &&
        feature import-marks=git.marks
        feature import-marks=git2.marks
        EOF
 
+       test_must_fail git fast-import --allow-unsafe-features <input
+'
+
+test_expect_success 'R: export-marks feature forbidden by default' '
+       echo "feature export-marks=git.marks" >input &&
        test_must_fail git fast-import <input
 '
 
@@ -2125,19 +2140,29 @@ test_expect_success 'R: export-marks feature results in a marks file being creat
 
        EOF
 
-       cat input | git fast-import &&
+       git fast-import --allow-unsafe-features <input &&
        grep :1 git.marks
 '
 
 test_expect_success 'R: export-marks options can be overridden by commandline options' '
-       cat input | git fast-import --export-marks=other.marks &&
-       grep :1 other.marks
+       cat >input <<-\EOF &&
+       feature export-marks=feature-sub/git.marks
+       blob
+       mark :1
+       data 3
+       hi
+
+       EOF
+       git fast-import --allow-unsafe-features \
+                       --export-marks=cmdline-sub/other.marks <input &&
+       grep :1 cmdline-sub/other.marks &&
+       test_path_is_missing feature-sub
 '
 
 test_expect_success 'R: catch typo in marks file name' '
        test_must_fail git fast-import --import-marks=nonexistent.marks </dev/null &&
        echo "feature import-marks=nonexistent.marks" |
-       test_must_fail git fast-import
+       test_must_fail git fast-import --allow-unsafe-features
 '
 
 test_expect_success 'R: import and output marks can be the same file' '
@@ -2193,7 +2218,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
        rm -f io.marks &&
        >expect &&
 
-       git fast-import --export-marks=io.marks <<-\EOF &&
+       git fast-import --export-marks=io.marks \
+                       --allow-unsafe-features <<-\EOF &&
        feature import-marks-if-exists=not_io.marks
        EOF
        test_cmp expect io.marks &&
@@ -2204,7 +2230,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
        echo ":1 $blob" >expect &&
        echo ":2 $blob" >>expect &&
 
-       git fast-import --export-marks=io.marks <<-\EOF &&
+       git fast-import --export-marks=io.marks \
+                       --allow-unsafe-features <<-\EOF &&
        feature import-marks-if-exists=io.marks
        blob
        mark :2
@@ -2217,7 +2244,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
        echo ":3 $blob" >>expect &&
 
        git fast-import --import-marks=io.marks \
-                       --export-marks=io.marks <<-\EOF &&
+                       --export-marks=io.marks \
+                       --allow-unsafe-features <<-\EOF &&
        feature import-marks-if-exists=not_io.marks
        blob
        mark :3
@@ -2230,7 +2258,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
        >expect &&
 
        git fast-import --import-marks-if-exists=not_io.marks \
-                       --export-marks=io.marks <<-\EOF &&
+                       --export-marks=io.marks \
+                       --allow-unsafe-features <<-\EOF &&
        feature import-marks-if-exists=io.marks
        EOF
        test_cmp expect io.marks
@@ -2242,7 +2271,7 @@ test_expect_success 'R: import to output marks works without any content' '
        feature export-marks=marks.new
        EOF
 
-       cat input | git fast-import &&
+       git fast-import --allow-unsafe-features <input &&
        test_cmp marks.out marks.new
 '
 
@@ -2252,7 +2281,7 @@ test_expect_success 'R: import marks prefers commandline marks file over the str
        feature export-marks=marks.new
        EOF
 
-       cat input | git fast-import --import-marks=marks.out &&
+       git fast-import --import-marks=marks.out --allow-unsafe-features <input &&
        test_cmp marks.out marks.new
 '
 
@@ -2265,7 +2294,8 @@ test_expect_success 'R: multiple --import-marks= should be honoured' '
 
        head -n2 marks.out > one.marks &&
        tail -n +3 marks.out > two.marks &&
-       git fast-import --import-marks=one.marks --import-marks=two.marks <input &&
+       git fast-import --import-marks=one.marks --import-marks=two.marks \
+               --allow-unsafe-features <input &&
        test_cmp marks.out combined.marks
 '
 
@@ -2278,7 +2308,7 @@ test_expect_success 'R: feature relative-marks should be honoured' '
 
        mkdir -p .git/info/fast-import/ &&
        cp marks.new .git/info/fast-import/relative.in &&
-       git fast-import <input &&
+       git fast-import --allow-unsafe-features <input &&
        test_cmp marks.new .git/info/fast-import/relative.out
 '
 
@@ -2290,7 +2320,7 @@ test_expect_success 'R: feature no-relative-marks should be honoured' '
        feature export-marks=non-relative.out
        EOF
 
-       git fast-import <input &&
+       git fast-import --allow-unsafe-features <input &&
        test_cmp marks.new non-relative.out
 '
 
@@ -2560,7 +2590,7 @@ test_expect_success 'R: quiet option results in no stats being output' '
 
        EOF
 
-       cat input | git fast-import 2> output &&
+       git fast-import 2>output <input &&
        test_must_be_empty output
 '
 
index 866ddf6..15b167d 100755 (executable)
@@ -421,9 +421,10 @@ test_expect_success 'directory becomes symlink'        '
 
 test_expect_success 'fast-export quotes pathnames' '
        git init crazy-paths &&
+       test_config -C crazy-paths core.protectNTFS false &&
        (cd crazy-paths &&
         blob=$(echo foo | git hash-object -w --stdin) &&
-        git update-index --add \
+        git -c core.protectNTFS=false update-index --add \
                --cacheinfo 100644 $blob "$(printf "path with\\nnewline")" \
                --cacheinfo 100644 $blob "path with \"quote\"" \
                --cacheinfo 100644 $blob "path with \\backslash" \
index c948d52..f24e776 100644 (file)
@@ -431,6 +431,7 @@ static int get_importer(struct transport *transport, struct child_process *fasti
        child_process_init(fastimport);
        fastimport->in = helper->out;
        argv_array_push(&fastimport->args, "fast-import");
+       argv_array_push(&fastimport->args, "--allow-unsafe-features");
        argv_array_push(&fastimport->args, debug ? "--stats" : "--quiet");
 
        if (data->bidi_import) {
index 684f0e3..7875fdc 100644 (file)
@@ -41,6 +41,12 @@ static int decode_tree_entry(struct tree_desc *desc, const char *buf, unsigned l
                strbuf_addstr(err, _("empty filename in tree entry"));
                return -1;
        }
+#ifdef GIT_WINDOWS_NATIVE
+       if (protect_ntfs && strchr(path, '\\')) {
+               strbuf_addf(err, _("filename in tree entry contains backslash: '%s'"), path);
+               return -1;
+       }
+#endif
        len = strlen(path) + 1;
 
        /* Initialize the descriptor entry */
index 71b70cc..26028b7 100644 (file)
@@ -1809,7 +1809,8 @@ static int merged_entry(const struct cache_entry *ce,
                invalidate_ce_path(old, o);
        }
 
-       do_add_entry(o, merge, update, CE_STAGEMASK);
+       if (do_add_entry(o, merge, update, CE_STAGEMASK) < 0)
+               return -1;
        return 1;
 }