From c90dc5284e5f72146c85141c9e0a69e813bdfc7f Mon Sep 17 00:00:00 2001 From: Sven Strickroth Date: Fri, 15 May 2026 16:27:51 +0200 Subject: [PATCH 1/3] Add support for "/*" wildcard in safe.directory Signed-off-by: Sven Strickroth --- src/libgit2/repository.c | 24 +++++++++++++-- tests/libgit2/repo/open.c | 62 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/libgit2/repository.c b/src/libgit2/repository.c index c08801e11..88c32fd9a 100644 --- a/src/libgit2/repository.c +++ b/src/libgit2/repository.c @@ -582,16 +582,29 @@ static int validate_ownership_cb(const git_config_entry *entry, void *payload) } else if (strcmp(entry->value, "*") == 0) { *data->is_safe = true; } else { + bool is_prefix = false; + if (git_str_sets(&data->tmp, entry->value) < 0) return -1; + /* + * A value ending with slash* is treated as a prefix match. + * Strip only the '*', leaving the trailing slash in place. + */ + if (data->tmp.size >= 2 && + data->tmp.ptr[data->tmp.size - 2] == '/' && + data->tmp.ptr[data->tmp.size - 1] == '*') { + is_prefix = true; + git_str_truncate(&data->tmp, data->tmp.size - 1); + } + if (!git_fs_path_is_root(data->tmp.ptr)) { /* Input must not have trailing backslash. */ if (!data->tmp.size || - data->tmp.ptr[data->tmp.size - 1] == '/') + (!is_prefix && data->tmp.ptr[data->tmp.size - 1] == '/')) return 0; - if (git_fs_path_to_dir(&data->tmp) < 0) + if (!is_prefix && git_fs_path_to_dir(&data->tmp) < 0) return -1; } @@ -623,7 +636,12 @@ static int validate_ownership_cb(const git_config_entry *entry, void *payload) if (strncmp(test_path, "%(prefix)//", strlen("%(prefix)//")) == 0) test_path += strlen("%(prefix)/"); - if (strcmp(test_path, data->repo_path) == 0) + if (is_prefix) { + size_t len = strlen(test_path); + + if (strncmp(test_path, data->repo_path, len) == 0) + *data->is_safe = true; + } else if (strcmp(test_path, data->repo_path) == 0) *data->is_safe = true; } diff --git a/tests/libgit2/repo/open.c b/tests/libgit2/repo/open.c index 52b14ed5f..8f766d275 100644 --- a/tests/libgit2/repo/open.c +++ b/tests/libgit2/repo/open.c @@ -659,6 +659,36 @@ void test_repo_open__can_wildcard_allowlist_with_problematic_ownership(void) cl_git_pass(test_safe_path("*")); } +void test_repo_open__can_allowlist_dirs_wildcard(void) +{ + git_str path = GIT_STR_INIT; + + cl_git_pass(git_str_printf( + &path, "%s/*", clar_sandbox_path())); + cl_git_pass(test_safe_path(path.ptr)); + git_str_dispose(&path); +} + +void test_repo_open__can_allowlist_dirs_wildcard_fail(void) +{ + git_str path = GIT_STR_INIT; + + cl_git_pass(git_str_printf( + &path, "%s/%s*", clar_sandbox_path(), "empty_standard_repo")); + cl_git_fail_with(GIT_EOWNER, test_safe_path(path.ptr)); + git_str_dispose(&path); +} + +void test_repo_open__can_allowlist_dirs_wildcard_fail2(void) +{ + git_str path = GIT_STR_INIT; + + cl_git_pass(git_str_printf( + &path, "%s/%s*", clar_sandbox_path(), "empty_standard_repo")); + cl_git_fail_with(GIT_EOWNER, test_safe_path(path.ptr)); + git_str_dispose(&path); +} + void test_repo_open__can_allowlist_bare_gitdir(void) { git_str path = GIT_STR_INIT; @@ -669,6 +699,38 @@ void test_repo_open__can_allowlist_bare_gitdir(void) git_str_dispose(&path); } +void test_repo_open__can_allowlist_bare_wildcard_gitdir(void) +{ + git_str path = GIT_STR_INIT; + + cl_git_pass(git_str_printf( + &path, "%s/*", clar_sandbox_path())); + cl_git_pass(test_bare_safe_path(path.ptr)); + + git_str_dispose(&path); +} + +void test_repo_open__can_allowlist_bare_wildcard_gitdir_fail(void) +{ + git_str path = GIT_STR_INIT; + + cl_git_pass(git_str_printf(&path, "%s*", clar_sandbox_path())); + cl_git_fail_with(GIT_EOWNER, test_bare_safe_path(path.ptr)); + + git_str_dispose(&path); +} + +void test_repo_open__can_allowlist_bare_wildcard_gitdir_fail2(void) +{ + git_str path = GIT_STR_INIT; + + cl_git_pass(git_str_printf( + &path, "%s/%s*", clar_sandbox_path(), "testrepo.git")); + cl_git_fail_with(GIT_EOWNER, test_bare_safe_path(path.ptr)); + + git_str_dispose(&path); +} + void test_repo_open__can_wildcard_allowlist_bare_gitdir(void) { cl_git_pass(test_bare_safe_path("*")); From 2eed889d8a006693a892024e8ffcd735262ab76d Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 1 Jun 2026 23:24:45 +0100 Subject: [PATCH 2/3] repo: improve wildcard safe.directory tests --- tests/libgit2/repo/open.c | 51 ++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/tests/libgit2/repo/open.c b/tests/libgit2/repo/open.c index 8f766d275..c2edb3bc0 100644 --- a/tests/libgit2/repo/open.c +++ b/tests/libgit2/repo/open.c @@ -669,17 +669,7 @@ void test_repo_open__can_allowlist_dirs_wildcard(void) git_str_dispose(&path); } -void test_repo_open__can_allowlist_dirs_wildcard_fail(void) -{ - git_str path = GIT_STR_INIT; - - cl_git_pass(git_str_printf( - &path, "%s/%s*", clar_sandbox_path(), "empty_standard_repo")); - cl_git_fail_with(GIT_EOWNER, test_safe_path(path.ptr)); - git_str_dispose(&path); -} - -void test_repo_open__can_allowlist_dirs_wildcard_fail2(void) +void test_repo_open__allowlist_dirs_cannot_have_wildcard_suffix(void) { git_str path = GIT_STR_INIT; @@ -710,17 +700,7 @@ void test_repo_open__can_allowlist_bare_wildcard_gitdir(void) git_str_dispose(&path); } -void test_repo_open__can_allowlist_bare_wildcard_gitdir_fail(void) -{ - git_str path = GIT_STR_INIT; - - cl_git_pass(git_str_printf(&path, "%s*", clar_sandbox_path())); - cl_git_fail_with(GIT_EOWNER, test_bare_safe_path(path.ptr)); - - git_str_dispose(&path); -} - -void test_repo_open__can_allowlist_bare_wildcard_gitdir_fail2(void) +void test_repo_open__allowlist_bare_dirs_cannot_have_wildcard_suffix(void) { git_str path = GIT_STR_INIT; @@ -731,6 +711,16 @@ void test_repo_open__can_allowlist_bare_wildcard_gitdir_fail2(void) git_str_dispose(&path); } +void test_repo_open__allowlist_relative_bare_dirs_cannot_have_wildcard_suffix(void) +{ + git_str path = GIT_STR_INIT; + + cl_git_pass(git_str_printf(&path, "%s*", clar_sandbox_path())); + cl_git_fail_with(GIT_EOWNER, test_bare_safe_path(path.ptr)); + + git_str_dispose(&path); +} + void test_repo_open__can_wildcard_allowlist_bare_gitdir(void) { cl_git_pass(test_bare_safe_path("*")); @@ -753,6 +743,23 @@ void test_repo_open__can_handle_prefixed_safe_paths(void) #endif } +void test_repo_open__can_handle_prefixed_wildcard_safe_paths(void) +{ +#ifndef GIT_WIN32 + git_str path = GIT_STR_INIT; + + /* + * Using "%(prefix)/" becomes "%(prefix)//tmp/foo" - so + * "%(prefix)/" is stripped and means the literal path + * follows. + */ + cl_git_pass(git_str_printf(&path, "%%(prefix)/%s/*", + clar_sandbox_path())); + cl_git_pass(test_safe_path(path.ptr)); + git_str_dispose(&path); +#endif +} + void test_repo_open__prefixed_safe_paths_must_have_two_slashes(void) { git_str path = GIT_STR_INIT; From 72d4157dff3e9dfa37fd00ab2e008fd598acbbb8 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 1 Jun 2026 23:39:07 +0100 Subject: [PATCH 3/3] repo: simplify safe.directory checks Use some standard libgit2 utility functions. --- src/libgit2/repository.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/libgit2/repository.c b/src/libgit2/repository.c index 88c32fd9a..e45809263 100644 --- a/src/libgit2/repository.c +++ b/src/libgit2/repository.c @@ -582,7 +582,7 @@ static int validate_ownership_cb(const git_config_entry *entry, void *payload) } else if (strcmp(entry->value, "*") == 0) { *data->is_safe = true; } else { - bool is_prefix = false; + bool is_prefix = false, match; if (git_str_sets(&data->tmp, entry->value) < 0) return -1; @@ -591,11 +591,9 @@ static int validate_ownership_cb(const git_config_entry *entry, void *payload) * A value ending with slash* is treated as a prefix match. * Strip only the '*', leaving the trailing slash in place. */ - if (data->tmp.size >= 2 && - data->tmp.ptr[data->tmp.size - 2] == '/' && - data->tmp.ptr[data->tmp.size - 1] == '*') { + if (git__suffixcmp(data->tmp.ptr, "/*") == 0) { is_prefix = true; - git_str_truncate(&data->tmp, data->tmp.size - 1); + git_str_shorten(&data->tmp, 1); } if (!git_fs_path_is_root(data->tmp.ptr)) { @@ -636,12 +634,11 @@ static int validate_ownership_cb(const git_config_entry *entry, void *payload) if (strncmp(test_path, "%(prefix)//", strlen("%(prefix)//")) == 0) test_path += strlen("%(prefix)/"); - if (is_prefix) { - size_t len = strlen(test_path); + match = is_prefix ? + (git__prefixcmp(data->repo_path, test_path) == 0) : + (strcmp(test_path, data->repo_path) == 0); - if (strncmp(test_path, data->repo_path, len) == 0) - *data->is_safe = true; - } else if (strcmp(test_path, data->repo_path) == 0) + if (match) *data->is_safe = true; }