]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Sync with 2.19.3
authorJohannes Schindelin <johannes.schindelin@gmx.de>
Wed, 4 Dec 2019 21:31:10 +0000 (22:31 +0100)
committerJohannes Schindelin <johannes.schindelin@gmx.de>
Fri, 6 Dec 2019 15:30:49 +0000 (16:30 +0100)
* maint-2.19: (34 commits)
  Git 2.19.3
  Git 2.18.2
  Git 2.17.3
  Git 2.16.6
  test-drop-caches: use `has_dos_drive_prefix()`
  Git 2.15.4
  Git 2.14.6
  mingw: handle `subst`-ed "DOS drives"
  mingw: refuse to access paths with trailing spaces or periods
  mingw: refuse to access paths with illegal characters
  unpack-trees: let merged_entry() pass through do_add_entry()'s errors
  quote-stress-test: offer to test quoting arguments for MSYS2 sh
  t6130/t9350: prepare for stringent Win32 path validation
  quote-stress-test: allow skipping some trials
  quote-stress-test: accept arguments to test via the command-line
  tests: add a helper to stress test argument quoting
  mingw: fix quoting of arguments
  Disallow dubiously-nested submodule git directories
  protect_ntfs: turn on NTFS protection by default
  path: also guard `.gitmodules` against NTFS Alternate Data Streams
  ...

40 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/RelNotes/2.16.6.txt [new file with mode: 0644]
Documentation/RelNotes/2.17.3.txt [new file with mode: 0644]
Documentation/RelNotes/2.18.2.txt [new file with mode: 0644]
Documentation/RelNotes/2.19.3.txt [new file with mode: 0644]
Documentation/git-fast-import.txt
Documentation/gitmodules.txt
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-drop-caches.c
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`.
diff --git a/Documentation/RelNotes/2.16.6.txt b/Documentation/RelNotes/2.16.6.txt
new file mode 100644 (file)
index 0000000..438306e
--- /dev/null
@@ -0,0 +1,8 @@
+Git v2.16.6 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.14.6 and in
+v2.15.4 addressing 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 those
+versions for details.
diff --git a/Documentation/RelNotes/2.17.3.txt b/Documentation/RelNotes/2.17.3.txt
new file mode 100644 (file)
index 0000000..5a46c94
--- /dev/null
@@ -0,0 +1,12 @@
+Git v2.17.3 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.14.6 and in
+v2.15.4 addressing 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 those
+versions for details.
+
+In addition, `git fsck` was taught to identify `.gitmodules` entries
+of the form `submodule.<name>.update=!command`, which have been
+disallowed in v2.15.4.
diff --git a/Documentation/RelNotes/2.18.2.txt b/Documentation/RelNotes/2.18.2.txt
new file mode 100644 (file)
index 0000000..98b168a
--- /dev/null
@@ -0,0 +1,8 @@
+Git v2.18.2 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.14.6, v2.15.4
+and in v2.17.3, addressing 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 those versions for details.
diff --git a/Documentation/RelNotes/2.19.3.txt b/Documentation/RelNotes/2.19.3.txt
new file mode 100644 (file)
index 0000000..92d7f89
--- /dev/null
@@ -0,0 +1,8 @@
+Git v2.19.3 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.14.6, v2.15.4
+and in v2.17.3, addressing 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 those versions for details.
index e81117d27f477591b92480352731fb0c4faf3410..9e3b9581d76d7e7146423d857f27f1067710b580 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 312b6f92592258b88efcaed36392df998bfaba0a..164995d1af3bf87bb3eabee0e92fcec12b95ca32 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 15b142d64640e29c10e62d565ac21adbaaeebca4..5d31bea55e0e0f22012c96757eb9106c9d1e5133 100644 (file)
@@ -768,7 +768,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 d38113a31aeb3838190b7339475c7454e45f3a89..b93d624a859c2e321f92adf7676451011e662465 100644 (file)
@@ -18,6 +18,7 @@
 #include "diffcore.h"
 #include "diff.h"
 #include "object-store.h"
+#include "dir.h"
 
 #define OPT_QUIET (1 << 0)
 #define OPT_CACHED (1 << 1)
@@ -1354,7 +1355,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 dissociate = 0;
+       int dissociate = 0, require_init = 0;
        char *sm_alternate = NULL, *error_strategy = NULL;
 
        struct option module_clone_options[] = {
@@ -1381,6 +1382,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()
        };
 
@@ -1408,6 +1411,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);
@@ -1419,6 +1426,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);
@@ -1531,6 +1540,7 @@ struct submodule_update_clone {
        int recommend_shallow;
        struct string_list references;
        int dissociate;
+       unsigned require_init;
        const char *depth;
        const char *recursive_prefix;
        const char *prefix;
@@ -1549,7 +1559,7 @@ struct submodule_update_clone {
        int max_jobs;
 };
 #define SUBMODULE_UPDATE_CLONE_INIT {0, MODULE_LIST_INIT, 0, \
-       SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, 0, \
+       SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, 0, 0, \
        NULL, NULL, NULL, \
        NULL, 0, 0, 0, NULL, 0, 0, 0}
 
@@ -1676,6 +1686,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);
@@ -1866,6 +1878,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 34b3880b29d57eee6d6ae0afbb786d7980e7fa3e..a010e0b69d36f51787c07a55ef35096adb81bf6f 100644 (file)
@@ -389,6 +389,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);
@@ -462,7 +468,7 @@ int mingw_open (const char *filename, int oflags, ...)
        typedef int (*open_fn_t)(wchar_t const *wfilename, 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];
        open_fn_t open_fn;
 
@@ -470,6 +476,11 @@ int mingw_open (const char *filename, int 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";
 
@@ -536,6 +547,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 ||
@@ -558,6 +574,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 ||
@@ -1051,7 +1072,7 @@ static const char *quote_arg(const char *arg)
                                p++;
                                len++;
                        }
-                       if (*p == '"')
+                       if (*p == '"' || !*p)
                                n += count*2 + 1;
                        continue;
                }
@@ -1073,16 +1094,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;
 }
 
@@ -2275,6 +2299,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);
@@ -2416,6 +2464,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 8c24ddaa3efc20e4454ebc87c51fa30316f64a22..163ae1b59e2a1d444daa0087174f9a347b47e6b0 100644 (file)
@@ -443,8 +443,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)
@@ -479,6 +479,20 @@ extern char *mingw_query_user_email(void);
 #include <inttypes.h>
 #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.
  *
index 3ee7da0e230c4c33e79ae2f3cf498a5cd7cc4881..85af9f9cf1081335b786ddab7907cebb37ceb66d 100644 (file)
@@ -396,7 +396,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
@@ -529,7 +528,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 += -DWIN32 -DPROTECT_NTFS_DEFAULT=1
+       BASIC_CFLAGS += -DWIN32
        EXTLIBS += -lws2_32
        GITLIBS += git.res
        PTHREAD_LIBS =
index 24281b608284ee74b262237c467ff054874d8a8e..79f1b3b24257a100cfd338eebdb465e0069e0953 100644 (file)
--- a/connect.c
+++ b/connect.c
@@ -514,7 +514,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 346559770773e923766c06593ada84c72815061f..c7b76d3bbb1143f870685ac13ea70a0d333c6121 100644 (file)
@@ -80,7 +80,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;
 const char *core_fsmonitor;
index 69886687ce95fd7dfe5363d0c776e21241b6b830..c8b372bc4abbfdd6b3b3e31cc12bfe2ac0d9e838 100644 (file)
@@ -364,6 +364,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;
@@ -1826,6 +1827,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);
@@ -3198,7 +3205,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;
 }
@@ -3239,7 +3245,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)
@@ -3282,10 +3287,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;
        }
@@ -3293,6 +3300,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;
@@ -3300,10 +3314,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 */
@@ -3430,6 +3447,20 @@ int cmd_main(int argc, const char **argv)
        avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*));
        marks = mem_pool_calloc(&fi_mem_pool, 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 68502ce85b11bf0ef4445f2b1688b3834301ba53..535f806c67e72875e91021c8ce3dab6b8d783c26 100644 (file)
--- a/fsck.c
+++ b/fsck.c
@@ -68,6 +68,7 @@ static struct oidset gitmodules_done = OIDSET_INIT;
        FUNC(GITMODULES_SYMLINK, ERROR) \
        FUNC(GITMODULES_URL, ERROR) \
        FUNC(GITMODULES_PATH, ERROR) \
+       FUNC(GITMODULES_UPDATE, ERROR) \
        /* warnings */ \
        FUNC(BAD_FILEMODE, WARN) \
        FUNC(EMPTY_NAME, WARN) \
@@ -605,7 +606,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);
@@ -627,6 +628,22 @@ static int fsck_tree(struct tree *item, struct fsck_options *options)
                                                 ".gitmodules is a symbolic link");
                }
 
+               if ((backslash = strchr(name, '\\'))) {
+                       while (backslash) {
+                               backslash++;
+                               has_dotgit |= is_ntfs_dotgit(backslash);
+                               if (is_ntfs_dotgitmodules(backslash)) {
+                                       if (!S_ISLNK(mode))
+                                               oidset_insert(&gitmodules_found, oid);
+                                       else
+                                               retval += report(options, &item->object,
+                                                                FSCK_MSG_GITMODULES_SYMLINK,
+                                                                ".gitmodules is a symbolic link");
+                               }
+                               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;
@@ -1000,6 +1017,12 @@ static int fsck_gitmodules_fn(const char *var, const char *value, void *vdata)
                                    FSCK_MSG_GITMODULES_PATH,
                                    "disallowed submodule path: %s",
                                    value);
+       if (!strcmp(key, "update") && value &&
+           parse_submodule_update_type(value) == SM_UPDATE_COMMAND)
+               data->ret |= report(data->options, data->obj,
+                                   FSCK_MSG_GITMODULES_UPDATE,
+                                   "disallowed submodule update setting: %s",
+                                   value);
        free(name);
 
        return 0;
index 09b0102cae8c8c0e39dc239003ca599a896730cf..d17360e89789403ece1d81278822e7469cd296a1 100644 (file)
@@ -385,6 +385,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 5e608f8bad305fea40e9063b854bf1be24ebd448..65d62c888c8f2288ca276b28744f51f773bee8f2 100755 (executable)
@@ -34,6 +34,7 @@ reference=
 cached=
 recursive=
 init=
+require_init=
 files=
 remote=
 nofetch=
@@ -457,6 +458,10 @@ cmd_update()
                -i|--init)
                        init=1
                        ;;
+               --require-init)
+                       init=1
+                       require_init=1
+                       ;;
                --remote)
                        remote=1
                        ;;
@@ -539,6 +544,7 @@ cmd_update()
                ${reference:+"$reference"} \
                ${dissociate:+"--dissociate"} \
                ${depth:+--depth "$depth"} \
+               ${require_init:+--require-init} \
                $recommend_shallow \
                $jobs \
                "$@" || echo "#unmatched" $?
diff --git a/path.c b/path.c
index dc3294c71e1e72ae2aae9164c790e27d1237e956..617545cd5d4390e4d171485b2ca13f010330b21e 100644 (file)
--- a/path.c
+++ b/path.c
@@ -1292,37 +1292,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,
@@ -1338,7 +1378,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 bd45dc3e24d7dc28820d26cc6e6d377f92fda46d..1d82dbdd65c8f01db24d2e40b702988a412f1034 100644 (file)
@@ -955,6 +955,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)
@@ -982,7 +985,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 52702c62d9e3a205c14bdd9f468509f98fe4ced1..8c00855ed8fc8b1751ff64ebb53621d0e4c78f1d 100644 (file)
@@ -398,6 +398,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;
@@ -475,8 +482,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 6415cc55807c7ed9ee4cbcaa57f41a5804b6e854..37d29bb252474e43f1a942a0d19e39697ed1ff21 100644 (file)
@@ -1732,6 +1732,47 @@ out:
        return ret;
 }
 
+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.
@@ -1740,7 +1781,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))
@@ -1758,10 +1799,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 a680214c01a5fda90a21af547389f6333388683c..ac206dc182a8e65d8fa066ece747593bf8b781ea 100644 (file)
@@ -124,6 +124,11 @@ int push_unpushed_submodules(struct repository *r,
  */
 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)
 int submodule_move_head(const char *path,
index f65e301f9ddc13f395b88a4b8cf994506beee292..7b4278462bb7166522369aa9d74147fa36c03835 100644 (file)
@@ -8,18 +8,21 @@ static int cmd_sync(void)
 {
        char Buffer[MAX_PATH];
        DWORD dwRet;
-       char szVolumeAccessPath[] = "\\\\.\\X:";
+       char szVolumeAccessPath[] = "\\\\.\\XXXX:";
        HANDLE hVolWrite;
-       int success = 0;
+       int success = 0, dos_drive_prefix;
 
        dwRet = GetCurrentDirectory(MAX_PATH, Buffer);
        if ((0 == dwRet) || (dwRet > MAX_PATH))
                return error("Error getting current directory");
 
-       if (!has_dos_drive_prefix(Buffer))
+       dos_drive_prefix = has_dos_drive_prefix(Buffer);
+       if (!dos_drive_prefix)
                return error("'%s': invalid drive letter", Buffer);
 
-       szVolumeAccessPath[4] = Buffer[0];
+       memcpy(szVolumeAccessPath, Buffer, dos_drive_prefix);
+       szVolumeAccessPath[dos_drive_prefix] = '\0';
+
        hVolWrite = CreateFile(szVolumeAccessPath, GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
        if (INVALID_HANDLE_VALUE == hVolWrite)
index ae091d9b3e63cb506b2530217d40048b301faaa3..e737a941d3bc1d58c768b2e2819d0759e17f83c2 100644 (file)
@@ -177,6 +177,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__path_utils(int argc, const char **argv)
 {
        if (argc == 3 && !strcmp(argv[1], "normalize_path_copy")) {
@@ -291,6 +384,26 @@ int cmd__path_utils(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 2cc93bb69c522d99491cd8a9e02e211b2c3df807..8579b1f7d13394b89dfff161b6f6558d498ac2f3 100644 (file)
@@ -13,8 +13,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,
@@ -50,11 +50,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-tool 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-tool", "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__run_command(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;
        while (!strcmp(argv[1], "env")) {
index c7b53e494ba43fdcff874a506468a4ecc0881a48..b193ed42050509bc3951e2faae22fbecf0d0b044 100755 (executable)
@@ -165,6 +165,15 @@ test_expect_success 'absolute path rejects the empty string' '
        test_must_fail test-tool 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-tool path-utils absolute_path "$path")" &&
+               test "$path" = "$absolute" || return 1
+       done
+'
+
 test_expect_success 'real path rejects the empty string' '
        test_must_fail test-tool path-utils real_path ""
 '
@@ -423,6 +432,9 @@ test_expect_success 'match .gitmodules' '
                ~1000000 \
                ~9999999 \
                \
+               .gitmodules:\$DATA \
+               "gitmod~4 . :\$DATA" \
+               \
                --not \
                ".gitmodules x"  \
                ".gitmodules .x" \
@@ -447,7 +459,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-tool 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 2f5a25d503861a5a03e60bdc1087f27f72c0e60e..da3376b3bb274b3632d204f225908fec701fa644 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 e20e8fa8301607b94cff7c42e83545f072d5191f..c767e2783b50fce0f60254b1b7e1b27a82587eea 100755 (executable)
@@ -453,6 +453,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 37760233a560559730fdf8c00a45c07d86250c00..ba7902c9cdcd0e6664f81b478b24596a4deb06dc 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 e87164aa8ffdba169ba6f91af99ae3d28f28459b..ad7d8fa69e52046a653ba5b3789ce2cbc9d66a2c 100755 (executable)
@@ -407,12 +407,26 @@ 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
+'
+
+test_expect_success 'fsck detects command in .gitmodules' '
+       git init command-in-gitmodules &&
+       (
+               cd command-in-gitmodules &&
+               git submodule add ../submodule submodule &&
+               test_commit adding-submodule &&
+
+               git config -f .gitmodules submodule.submodule.update "!false" &&
+               git add .gitmodules &&
+               test_commit configuring-update &&
+               test_must_fail git fsck
+       )
 '
 
 cat << EOF >expect
@@ -481,6 +495,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 &&
         git ls-files -s submodule >out &&
         H=$(cut -d" " -f2 out) &&
@@ -489,10 +506,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 293e2e1963962e49afa10882439b5106607439a1..33a9126ee0017df7345890a8b4506f721262eda5 100755 (executable)
@@ -191,4 +191,60 @@ test_expect_success 'fsck detects corrupt .gitmodules' '
        )
 '
 
+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 1cd2c1c1ea2d713c0b589df3ae9b34d43e5697a4..5ba041f537067dd9d16047a3569bb2b5fb3a4d51 100755 (executable)
@@ -46,4 +46,18 @@ test_expect_success 'fsck rejects unprotected dash' '
        grep gitmodulesUrl 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 756af8c4d6fbd1ba9fb2e4d6fd5b8ae9ddfc34e1..f7e7e94d7b6cfec6c625f6830b296b276572df66 100755 (executable)
@@ -25,4 +25,21 @@ test_expect_success 'fsck rejects unprotected dash' '
        grep gitmodulesPath 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 59a13b6a779b437fa377b30b2e0856f285f806b6..377c2b495833ac61f49d0933fcf4719d05fdd8dc 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' '
@@ -2192,7 +2217,8 @@ test_expect_success 'R: --import-marks-if-exists' '
 test_expect_success 'R: feature import-marks-if-exists' '
        rm -f io.marks &&
 
-       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_must_be_empty io.marks &&
@@ -2203,7 +2229,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
@@ -2216,7 +2243,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
@@ -2227,7 +2255,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
        test_cmp expect io.marks &&
 
        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_must_be_empty io.marks
@@ -2239,7 +2268,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
 '
 
@@ -2249,7 +2278,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
 '
 
@@ -2262,7 +2291,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
 '
 
@@ -2275,7 +2305,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
 '
 
@@ -2287,7 +2317,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
 '
 
@@ -2557,7 +2587,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 6a392e87bcc17712961c548c0ee84e11269aef0f..ef1e01805be86a4574ec8ea84bfc123baa9d67f5 100755 (executable)
@@ -418,9 +418,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 bf225c698fac81a9a94eff6d3371988ac4ff0bac..f79221cc9495b8766f190234377b0a113f6f1d70 100644 (file)
@@ -423,6 +423,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 79bafbd1a23c4a9e20ec623c084778904c534be7..bf07946ec49cf1fc0c25f8a9f46b1e48e1b9635e 100644 (file)
@@ -43,6 +43,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 7570df481bf69824e4b163a6c7a15985b72d1326..545d5668fdf79015784cafcfbc24322b68e8927c 100644 (file)
@@ -2073,7 +2073,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;
 }