Commit 318b825e76a9dc8afefc8274c5271747ad64d5a9

Edward Thomson 2016-02-16T17:11:46

index: allow read of index w/ illegal entries Allow `git_index_read` to handle reading existing indexes with illegal entries. Allow the low-level `git_index_add` to add properly formed `git_index_entry`s even if they contain paths that would be illegal for the current filesystem (eg, `AUX`). Continue to disallow `git_index_add_bypath` from adding entries that are illegal universally illegal (eg, `.git`, `foo/../bar`).

diff --git a/src/checkout.c b/src/checkout.c
index a92ad08..07b6bd6 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -1226,7 +1226,7 @@ static int checkout_verify_paths(
 	int action,
 	git_diff_delta *delta)
 {
-	unsigned int flags = GIT_PATH_REJECT_DEFAULTS | GIT_PATH_REJECT_DOT_GIT;
+	unsigned int flags = GIT_PATH_REJECT_WORKDIR_DEFAULTS;
 
 	if (action & CHECKOUT_ACTION__REMOVE) {
 		if (!git_path_isvalid(repo, delta->old_file.path, flags)) {
diff --git a/src/index.c b/src/index.c
index 5c7bd90..5728008 100644
--- a/src/index.c
+++ b/src/index.c
@@ -891,17 +891,31 @@ static void index_entry_adjust_namemask(
 		entry->flags |= GIT_IDXENTRY_NAMEMASK;
 }
 
+/* When `from_workdir` is true, we will validate the paths to avoid placing
+ * paths that are invalid for the working directory on the current filesystem
+ * (eg, on Windows, we will disallow `GIT~1`, `AUX`, `COM1`, etc).  This
+ * function will *always* prevent `.git` and directory traversal `../` from
+ * being added to the index.
+ */
 static int index_entry_create(
 	git_index_entry **out,
 	git_repository *repo,
-	const char *path)
+	const char *path,
+	bool from_workdir)
 {
 	size_t pathlen = strlen(path), alloclen;
 	struct entry_internal *entry;
+	unsigned int path_valid_flags = GIT_PATH_REJECT_INDEX_DEFAULTS;
+
+	/* always reject placing `.git` in the index and directory traversal.
+	 * when requested, disallow platform-specific filenames and upgrade to
+	 * the platform-specific `.git` tests (eg, `git~1`, etc).
+	 */
+	if (from_workdir)
+		path_valid_flags |= GIT_PATH_REJECT_WORKDIR_DEFAULTS;
 
-	if (!git_path_isvalid(repo, path,
-		GIT_PATH_REJECT_DEFAULTS | GIT_PATH_REJECT_DOT_GIT)) {
-		giterr_set(GITERR_INDEX, "Invalid path: '%s'", path);
+	if (!git_path_isvalid(repo, path, path_valid_flags)) {
+		giterr_set(GITERR_INDEX, "invalid path: '%s'", path);
 		return -1;
 	}
 
@@ -933,7 +947,7 @@ static int index_entry_init(
 			"Could not initialize index entry. "
 			"Index is not backed up by an existing repository.");
 
-	if (index_entry_create(&entry, INDEX_OWNER(index), rel_path) < 0)
+	if (index_entry_create(&entry, INDEX_OWNER(index), rel_path, true) < 0)
 		return -1;
 
 	/* write the blob to disk and get the oid and stat info */
@@ -1013,7 +1027,7 @@ static int index_entry_dup(
 	git_index *index,
 	const git_index_entry *src)
 {
-	if (index_entry_create(out, INDEX_OWNER(index), src->path) < 0)
+	if (index_entry_create(out, INDEX_OWNER(index), src->path, false) < 0)
 		return -1;
 
 	index_entry_cpy(*out, src);
@@ -1035,7 +1049,7 @@ static int index_entry_dup_nocache(
 	git_index *index,
 	const git_index_entry *src)
 {
-	if (index_entry_create(out, INDEX_OWNER(index), src->path) < 0)
+	if (index_entry_create(out, INDEX_OWNER(index), src->path, false) < 0)
 		return -1;
 
 	index_entry_cpy_nocache(*out, src);
@@ -1447,7 +1461,7 @@ static int add_repo_as_submodule(git_index_entry **out, git_index *index, const 
 	struct stat st;
 	int error;
 
-	if (index_entry_create(&entry, INDEX_OWNER(index), path) < 0)
+	if (index_entry_create(&entry, INDEX_OWNER(index), path, true) < 0)
 		return -1;
 
 	if ((error = git_buf_joinpath(&abspath, git_repository_workdir(repo), path)) < 0)
@@ -2884,7 +2898,7 @@ static int read_tree_cb(
 	if (git_buf_joinpath(&path, root, tentry->filename) < 0)
 		return -1;
 
-	if (index_entry_create(&entry, INDEX_OWNER(data->index), path.ptr) < 0)
+	if (index_entry_create(&entry, INDEX_OWNER(data->index), path.ptr, false) < 0)
 		return -1;
 
 	entry->mode = tentry->attr;
diff --git a/src/path.c b/src/path.c
index 18b4f03..852ef57 100644
--- a/src/path.c
+++ b/src/path.c
@@ -1630,9 +1630,12 @@ static bool verify_component(
 		!verify_dotgit_ntfs(repo, component, len))
 		return false;
 
+	/* don't bother rerunning the `.git` test if we ran the HFS or NTFS
+	 * specific tests, they would have already rejected `.git`.
+	 */
 	if ((flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 &&
 		(flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 &&
-		(flags & GIT_PATH_REJECT_DOT_GIT) &&
+		(flags & GIT_PATH_REJECT_DOT_GIT_LITERAL) &&
 		len == 4 &&
 		component[0] == '.' &&
 		(component[1] == 'g' || component[1] == 'G') &&
@@ -1649,6 +1652,8 @@ GIT_INLINE(unsigned int) dotgit_flags(
 {
 	int protectHFS = 0, protectNTFS = 0;
 
+	flags |= GIT_PATH_REJECT_DOT_GIT_LITERAL;
+
 #ifdef __APPLE__
 	protectHFS = 1;
 #endif
diff --git a/src/path.h b/src/path.h
index 7e156fc..875c8cb 100644
--- a/src/path.h
+++ b/src/path.h
@@ -564,15 +564,16 @@ extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or
 #define GIT_PATH_REJECT_TRAILING_COLON     (1 << 6)
 #define GIT_PATH_REJECT_DOS_PATHS          (1 << 7)
 #define GIT_PATH_REJECT_NT_CHARS           (1 << 8)
-#define GIT_PATH_REJECT_DOT_GIT_HFS        (1 << 9)
-#define GIT_PATH_REJECT_DOT_GIT_NTFS       (1 << 10)
+#define GIT_PATH_REJECT_DOT_GIT_LITERAL    (1 << 9)
+#define GIT_PATH_REJECT_DOT_GIT_HFS        (1 << 10)
+#define GIT_PATH_REJECT_DOT_GIT_NTFS       (1 << 11)
 
 /* Default path safety for writing files to disk: since we use the
  * Win32 "File Namespace" APIs ("\\?\") we need to protect from
  * paths that the normal Win32 APIs would not write.
  */
 #ifdef GIT_WIN32
-# define GIT_PATH_REJECT_DEFAULTS \
+# define GIT_PATH_REJECT_FILESYSTEM_DEFAULTS \
 	GIT_PATH_REJECT_TRAVERSAL | \
 	GIT_PATH_REJECT_BACKSLASH | \
 	GIT_PATH_REJECT_TRAILING_DOT | \
@@ -581,9 +582,18 @@ extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or
 	GIT_PATH_REJECT_DOS_PATHS | \
 	GIT_PATH_REJECT_NT_CHARS
 #else
-# define GIT_PATH_REJECT_DEFAULTS GIT_PATH_REJECT_TRAVERSAL
+# define GIT_PATH_REJECT_FILESYSTEM_DEFAULTS \
+	GIT_PATH_REJECT_TRAVERSAL
 #endif
 
+ /* Paths that should never be written into the working directory. */
+#define GIT_PATH_REJECT_WORKDIR_DEFAULTS \
+	GIT_PATH_REJECT_FILESYSTEM_DEFAULTS | GIT_PATH_REJECT_DOT_GIT
+
+/* Paths that should never be written to the index. */
+#define GIT_PATH_REJECT_INDEX_DEFAULTS \
+	GIT_PATH_REJECT_TRAVERSAL | GIT_PATH_REJECT_DOT_GIT
+
 /*
  * Determine whether a path is a valid git path or not - this must not contain
  * a '.' or '..' component, or a component that is ".git" (in any case).
diff --git a/src/refdb_fs.c b/src/refdb_fs.c
index 85b5034..1348c67 100644
--- a/src/refdb_fs.c
+++ b/src/refdb_fs.c
@@ -717,7 +717,7 @@ static int loose_lock(git_filebuf *file, refdb_fs_backend *backend, const char *
 
 	assert(file && backend && name);
 
-	if (!git_path_isvalid(backend->repo, name, GIT_PATH_REJECT_DEFAULTS)) {
+	if (!git_path_isvalid(backend->repo, name, GIT_PATH_REJECT_FILESYSTEM_DEFAULTS)) {
 		giterr_set(GITERR_INVALID, "Invalid reference name '%s'.", name);
 		return GIT_EINVALIDSPEC;
 	}
@@ -1672,7 +1672,7 @@ static int lock_reflog(git_filebuf *file, refdb_fs_backend *backend, const char 
 
 	repo = backend->repo;
 
-	if (!git_path_isvalid(backend->repo, refname, GIT_PATH_REJECT_DEFAULTS)) {
+	if (!git_path_isvalid(backend->repo, refname, GIT_PATH_REJECT_FILESYSTEM_DEFAULTS)) {
 		giterr_set(GITERR_INVALID, "Invalid reference name '%s'.", refname);
 		return GIT_EINVALIDSPEC;
 	}
diff --git a/tests/path/core.c b/tests/path/core.c
index 064f149..3dccfe5 100644
--- a/tests/path/core.c
+++ b/tests/path/core.c
@@ -105,12 +105,12 @@ void test_path_core__isvalid_dot_git(void)
 	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/.GIT/bar", 0));
 	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar/.Git", 0));
 
-	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git", GIT_PATH_REJECT_DOT_GIT));
-	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git/foo", GIT_PATH_REJECT_DOT_GIT));
-	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.git", GIT_PATH_REJECT_DOT_GIT));
-	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.git/bar", GIT_PATH_REJECT_DOT_GIT));
-	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.GIT/bar", GIT_PATH_REJECT_DOT_GIT));
-	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar/.Git", GIT_PATH_REJECT_DOT_GIT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git", GIT_PATH_REJECT_DOT_GIT_LITERAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git/foo", GIT_PATH_REJECT_DOT_GIT_LITERAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.git", GIT_PATH_REJECT_DOT_GIT_LITERAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.git/bar", GIT_PATH_REJECT_DOT_GIT_LITERAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.GIT/bar", GIT_PATH_REJECT_DOT_GIT_LITERAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar/.Git", GIT_PATH_REJECT_DOT_GIT_LITERAL));
 
 	cl_assert_equal_b(true, git_path_isvalid(NULL, "!git", 0));
 	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/!git", 0));