Commit c15ed350bb95fa7b07a70c60212f55be169ad12e

Edward Thomson 2021-04-26T12:23:25

repo: validate repository paths Ensure that a repository's path (at initialization or open time) is valid. On Windows systems, this means that the longest known path beneath the repository will fit within MAX_PATH: this is a lock file for a loose object within the repository itself. Other paths, like a very long loose reference, may fail to be opened after the repository is opened. These variable length paths will be checked when they are accessed themselves. This new functionality is done at open to prevent needlessly checking every file in the gitdir (eg, `MERGE_HEAD`) for its length when we could instead check once at repository open time.

diff --git a/src/repository.c b/src/repository.c
index 87f5c1a..8da6ccb 100644
--- a/src/repository.c
+++ b/src/repository.c
@@ -187,7 +187,7 @@ void git_repository_free(git_repository *repo)
 }
 
 /* Check if we have a separate commondir (e.g. we have a worktree) */
-static int lookup_commondir(git_buf *out, git_buf *repository_path)
+static int lookup_commondir(bool *separate, git_buf *commondir, git_buf *repository_path)
 {
 	git_buf common_link  = GIT_BUF_INIT;
 	int error;
@@ -197,33 +197,52 @@ static int lookup_commondir(git_buf *out, git_buf *repository_path)
 	 * common path, but it needs a trailing slash.
 	 */
 	if (!git_path_contains_file(repository_path, GIT_COMMONDIR_FILE)) {
-		if ((error = git_buf_set(out, repository_path->ptr, repository_path->size)) == 0)
-		    error = git_path_to_dir(out);
+		if ((error = git_buf_set(commondir, repository_path->ptr, repository_path->size)) == 0)
+		    error = git_path_to_dir(commondir);
 
+		*separate = false;
 		goto done;
 	}
 
+	*separate = true;
+
 	if ((error = git_buf_joinpath(&common_link, repository_path->ptr, GIT_COMMONDIR_FILE)) < 0 ||
 	    (error = git_futils_readbuffer(&common_link, common_link.ptr)) < 0)
 		goto done;
 
 	git_buf_rtrim(&common_link);
 	if (git_path_is_relative(common_link.ptr)) {
-		if ((error = git_buf_joinpath(out, repository_path->ptr, common_link.ptr)) < 0)
+		if ((error = git_buf_joinpath(commondir, repository_path->ptr, common_link.ptr)) < 0)
 			goto done;
 	} else {
-		git_buf_swap(out, &common_link);
+		git_buf_swap(commondir, &common_link);
 	}
 
 	git_buf_dispose(&common_link);
 
 	/* Make sure the commondir path always has a trailing slash */
-	error = git_path_prettify_dir(out, out->ptr, NULL);
+	error = git_path_prettify_dir(commondir, commondir->ptr, NULL);
 
 done:
 	return error;
 }
 
+GIT_INLINE(int) validate_repo_path(git_buf *path)
+{
+	/*
+	 * The longest static path in a repository (or commondir) is the
+	 * packed refs file.  (Loose refs may be longer since they
+	 * include the reference name, but will be validated when the
+	 * path is constructed.)
+	 */
+	static size_t suffix_len =
+		CONST_STRLEN("objects/pack/pack-.pack.lock") +
+		GIT_OID_HEXSZ;
+
+	return git_path_validate_filesystem_with_suffix(
+		path->ptr, path->size, suffix_len);
+}
+
 /*
  * Git repository open methods
  *
@@ -231,11 +250,12 @@ done:
  */
 static int is_valid_repository_path(bool *out, git_buf *repository_path, git_buf *common_path)
 {
+	bool separate_commondir = false;
 	int error;
 
 	*out = false;
 
-	if ((error = lookup_commondir(common_path, repository_path)) < 0)
+	if ((error = lookup_commondir(&separate_commondir, common_path, repository_path)) < 0)
 		return error;
 
 	/* Ensure HEAD file exists */
@@ -248,6 +268,12 @@ static int is_valid_repository_path(bool *out, git_buf *repository_path, git_buf
 	if (git_path_contains_dir(common_path, GIT_REFS_DIR) == false)
 		return 0;
 
+	/* Ensure the repo (and commondir) are valid paths */
+	if ((error = validate_repo_path(common_path)) < 0 ||
+	    (separate_commondir &&
+	     (error = validate_repo_path(repository_path)) < 0))
+		return error;
+
 	*out = true;
 	return 0;
 }
diff --git a/tests/repo/init.c b/tests/repo/init.c
index 01371ba..1aa326f 100644
--- a/tests/repo/init.c
+++ b/tests/repo/init.c
@@ -704,3 +704,35 @@ void test_repo_init__defaultbranch_config_empty(void)
 
 	git_reference_free(head);
 }
+
+void test_repo_init__longpath(void)
+{
+#ifdef GIT_WIN32
+	size_t padding = CONST_STRLEN("objects/pack/pack-.pack.lock") + GIT_OID_HEXSZ;
+	size_t max, i;
+	git_buf path = GIT_BUF_INIT;
+	git_repository *one = NULL, *two = NULL;
+
+	/*
+	 * Files within repositories need to fit within MAX_PATH;
+	 * that means a repo path must be at most (MAX_PATH - 18).
+	 */
+	cl_git_pass(git_buf_puts(&path, clar_sandbox_path()));
+	cl_git_pass(git_buf_putc(&path, '/'));
+
+	max = ((MAX_PATH) - path.size) - padding;
+
+	for (i = 0; i < max - 1; i++)
+		cl_git_pass(git_buf_putc(&path, 'a'));
+
+	cl_git_pass(git_repository_init(&one, path.ptr, 1));
+
+	/* Paths longer than this are rejected */
+	cl_git_pass(git_buf_putc(&path, 'z'));
+	cl_git_fail(git_repository_init(&two, path.ptr, 1));
+
+	git_repository_free(one);
+	git_repository_free(two);
+	git_buf_dispose(&path);
+#endif
+}