diff --git a/Documentation/RelNotes/2.14.6.txt b/Documentation/RelNotes/2.14.6.txt new file mode 100644 index 0000000000..72b7af6799 --- /dev/null +++ b/Documentation/RelNotes/2.14.6.txt @@ -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 : + . 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 index 0000000000..dc241cba34 --- /dev/null +++ b/Documentation/RelNotes/2.15.4.txt @@ -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..update=!command`. diff --git a/Documentation/RelNotes/2.16.6.txt b/Documentation/RelNotes/2.16.6.txt new file mode 100644 index 0000000000..438306e60b --- /dev/null +++ b/Documentation/RelNotes/2.16.6.txt @@ -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 index 0000000000..5a46c94271 --- /dev/null +++ b/Documentation/RelNotes/2.17.3.txt @@ -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..update=!command`, which have been +disallowed in v2.15.4. diff --git a/Documentation/git-fast-import.txt b/Documentation/git-fast-import.txt index e81117d27f..9e3b9581d7 100644 --- a/Documentation/git-fast-import.txt +++ b/Documentation/git-fast-import.txt @@ -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 ~~~~~~~~~~~~~~~~~~~~~ diff --git a/Documentation/gitmodules.txt b/Documentation/gitmodules.txt index 4d63def206..4caf5d8e3f 100644 --- a/Documentation/gitmodules.txt +++ b/Documentation/gitmodules.txt @@ -44,9 +44,8 @@ submodule..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..branch:: A remote branch name for tracking updates in the upstream submodule. diff --git a/builtin/clone.c b/builtin/clone.c index 99e73dae85..d0e0ff192b 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -765,7 +765,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"); diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 1902b6c319..05e5ed32b0 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -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) @@ -1203,7 +1204,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[] = { @@ -1230,6 +1231,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() }; @@ -1257,6 +1260,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); @@ -1268,6 +1275,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); @@ -1316,6 +1325,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; @@ -1331,7 +1341,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, 0, \ + SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, 0, 0, \ NULL, NULL, NULL, \ STRING_LIST_INIT_DUP, 0, NULL, 0, 0} @@ -1450,6 +1460,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); @@ -1605,6 +1617,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() }; diff --git a/compat/mingw.c b/compat/mingw.c index 6ded1c859f..0c0c474221 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -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 || @@ -883,7 +904,7 @@ static const char *quote_arg(const char *arg) p++; len++; } - if (*p == '"') + if (*p == '"' || !*p) n += count*2 + 1; continue; } @@ -905,16 +926,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; } @@ -1976,6 +2000,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); @@ -2117,6 +2165,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). diff --git a/compat/mingw.h b/compat/mingw.h index 571019d0bd..5b43e6557e 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -397,8 +397,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) @@ -431,6 +431,20 @@ int mingw_offset_1st_component(const char *path); #include #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. * diff --git a/config.mak.uname b/config.mak.uname index 684fc5bf02..c3f2c0fc17 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -389,7 +389,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 @@ -528,7 +527,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 = diff --git a/connect.c b/connect.c index 968e91b18c..a347f0ac10 100644 --- 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) diff --git a/environment.c b/environment.c index 2a6de2330b..8964b407a9 100644 --- a/environment.c +++ b/environment.c @@ -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; diff --git a/fast-import.c b/fast-import.c index 4d55910ab9..82a289d7a5 100644 --- a/fast-import.c +++ b/fast-import.c @@ -363,6 +363,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; @@ -1823,6 +1824,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); @@ -3195,7 +3202,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; } @@ -3236,7 +3242,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) @@ -3279,10 +3284,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; } @@ -3290,6 +3297,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; @@ -3297,10 +3311,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 */ @@ -3427,6 +3444,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 51f61ffbc2..6ccc6388fa 100644 --- a/fsck.c +++ b/fsck.c @@ -66,6 +66,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) \ @@ -568,7 +569,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); @@ -590,6 +591,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; @@ -963,6 +980,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; diff --git a/git-compat-util.h b/git-compat-util.h index 9a64998b24..f5bc4e0976 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -374,6 +374,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) { diff --git a/git-submodule.sh b/git-submodule.sh index 78073cd87d..74f5fe6a86 100755 --- a/git-submodule.sh +++ b/git-submodule.sh @@ -34,6 +34,7 @@ reference= cached= recursive= init= +require_init= files= remote= nofetch= @@ -482,6 +483,10 @@ cmd_update() -i|--init) init=1 ;; + --require-init) + init=1 + require_init=1 + ;; --remote) remote=1 ;; @@ -564,6 +569,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 7f109f6618..9a4b9e7d0e 100644 --- a/path.c +++ b/path.c @@ -1290,37 +1290,77 @@ int daemon_avoid_alias(const char *p) } } -static int only_spaces_and_periods(const char *path, size_t len, size_t skip) -{ - if (len < skip) - return 0; - len -= skip; - path += skip; - while (len-- > 0) { - char c = *(path++); - if (c != ' ' && c != '.') - return 0; - } - return 1; -} - +/* + * 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 + * `::`. 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) { - size_t len; + char c; - 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; - } + /* + * 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; + + for (;;) { + c = *(name++); + if (!c || c == '\\' || c == '/' || c == ':') + return 1; + if (c != '.' && c != ' ') + return 0; + } } static int is_ntfs_dot_generic(const char *name, @@ -1336,7 +1376,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; diff --git a/read-cache.c b/read-cache.c index 372588260e..eaa2709414 100644 --- a/read-cache.c +++ b/read-cache.c @@ -868,6 +868,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) @@ -895,7 +898,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++; } } diff --git a/submodule-config.c b/submodule-config.c index 6b212bae31..91c9479cd3 100644 --- a/submodule-config.c +++ b/submodule-config.c @@ -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, diff --git a/submodule.c b/submodule.c index 939d6870ec..893083cbf0 100644 --- a/submodule.c +++ b/submodule.c @@ -1703,6 +1703,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. @@ -1711,7 +1752,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)) @@ -1729,10 +1770,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, diff --git a/submodule.h b/submodule.h index 7856b8a0b3..17347ace2e 100644 --- a/submodule.h +++ b/submodule.h @@ -114,6 +114,11 @@ extern int push_unpushed_submodules(struct oid_array *commits, */ 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, diff --git a/t/helper/test-drop-caches.c b/t/helper/test-drop-caches.c index d6bcfddf13..7b4278462b 100644 --- a/t/helper/test-drop-caches.c +++ b/t/helper/test-drop-caches.c @@ -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 ((Buffer[0] < 'A') || (Buffer[0] > 'Z')) - return error("Invalid drive letter '%c'", Buffer[0]); + dos_drive_prefix = has_dos_drive_prefix(Buffer); + if (!dos_drive_prefix) + return error("'%s': invalid drive letter", Buffer); + + memcpy(szVolumeAccessPath, Buffer, dos_drive_prefix); + szVolumeAccessPath[dos_drive_prefix] = '\0'; - szVolumeAccessPath[4] = Buffer[0]; hVolWrite = CreateFile(szVolumeAccessPath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (INVALID_HANDLE_VALUE == hVolWrite) diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c index ae091d9b3e..e737a941d3 100644 --- a/t/helper/test-path-utils.c +++ b/t/helper/test-path-utils.c @@ -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; diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c index 2cc93bb69c..8579b1f7d1 100644 --- a/t/helper/test-run-command.c +++ b/t/helper/test-run-command.c @@ -13,8 +13,8 @@ #include "run-command.h" #include "argv-array.h" #include "strbuf.h" -#include -#include +#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 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 ", + 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")) { diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 21a8b53132..f7ab3d5d2f 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -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 ':\\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 "" ' @@ -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-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 diff --git a/t/t1014-read-tree-confusing.sh b/t/t1014-read-tree-confusing.sh index 2f5a25d503..da3376b3bb 100755 --- a/t/t1014-read-tree-confusing.sh +++ b/t/t1014-read-tree-confusing.sh @@ -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' ' diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh index 91fd71444d..561a9b5c78 100755 --- a/t/t1450-fsck.sh +++ b/t/t1450-fsck.sh @@ -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 && diff --git a/t/t6130-pathspec-noglob.sh b/t/t6130-pathspec-noglob.sh index 658353277e..4129d9fd9a 100755 --- a/t/t6130-pathspec-noglob.sh +++ b/t/t6130-pathspec-noglob.sh @@ -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 && diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index 9e0d31700e..9a9dff5dea 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -406,12 +406,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 @@ -480,6 +494,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 +504,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 ) ' diff --git a/t/t7415-submodule-names.sh b/t/t7415-submodule-names.sh index b68c5f5e85..31d1e2d512 100755 --- a/t/t7415-submodule-names.sh +++ b/t/t7415-submodule-names.sh @@ -176,4 +176,60 @@ test_expect_success 'fsck detects non-blob .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 diff --git a/t/t7416-submodule-dash-url.sh b/t/t7416-submodule-dash-url.sh index 1cd2c1c1ea..5ba041f537 100755 --- a/t/t7416-submodule-dash-url.sh +++ b/t/t7416-submodule-dash-url.sh @@ -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 diff --git a/t/t7417-submodule-path-url.sh b/t/t7417-submodule-path-url.sh index 756af8c4d6..f7e7e94d7b 100755 --- a/t/t7417-submodule-path-url.sh +++ b/t/t7417-submodule-path-url.sh @@ -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.new && + tree=$(git -C super mktree err && + test_i18ngrep "sub " err +' + test_done diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh index 9e7f96223d..c4a06c587f 100755 --- a/t/t9300-fast-import.sh +++ b/t/t9300-fast-import.sh @@ -2106,12 +2106,27 @@ test_expect_success 'R: abort on receiving feature after data command' ' test_must_fail git fast-import git.marks && + echo "feature import-marks=git.marks" >input && + test_must_fail git fast-import input && + test_must_fail git fast-import 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_must_fail git fast-import 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 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 one.marks && tail -n +3 marks.out > two.marks && - git fast-import --import-marks=one.marks --import-marks=two.marks output && + git fast-import 2>output 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) { diff --git a/tree-walk.c b/tree-walk.c index 8f5090862b..766131039b 100644 --- a/tree-walk.c +++ b/tree-walk.c @@ -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 */ diff --git a/unpack-trees.c b/unpack-trees.c index 3a85a02a77..a733555def 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -1856,7 +1856,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; }