From 44701007b4b1ede450b4a771f63dd68c0e303373 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Wed, 15 Oct 2025 21:54:09 +0300 Subject: [PATCH] libfetchers/git-utils: Be more correct about validating refnames Turns out there's a much better API for this that doesn't have the footguns of the previous method. isLegalRefName is somewhat of a misnomer, since it's mainly used to validate user inputs that can be either references, branch names, psedorefs or tags. (cherry picked from commit 5d1178b817a5c14cca96a57a11a4161ba70d83aa) --- src/libfetchers-tests/git-utils.cc | 6 ++ src/libfetchers/git-utils.cc | 58 +++++-------------- .../include/nix/fetchers/git-utils.hh | 7 ++- tests/functional/fetchGitRefs.sh | 3 + 4 files changed, 29 insertions(+), 45 deletions(-) diff --git a/src/libfetchers-tests/git-utils.cc b/src/libfetchers-tests/git-utils.cc index f9fae23da..774934d26 100644 --- a/src/libfetchers-tests/git-utils.cc +++ b/src/libfetchers-tests/git-utils.cc @@ -175,6 +175,12 @@ TEST_F(GitUtilsTest, peel_reference) TEST(GitUtils, isLegalRefName) { + ASSERT_TRUE(isLegalRefName("A/b")); + ASSERT_TRUE(isLegalRefName("AaA/b")); + ASSERT_TRUE(isLegalRefName("FOO/BAR/BAZ")); + ASSERT_TRUE(isLegalRefName("HEAD")); + ASSERT_TRUE(isLegalRefName("refs/tags/1.2.3")); + ASSERT_TRUE(isLegalRefName("refs/heads/master")); ASSERT_TRUE(isLegalRefName("foox")); ASSERT_TRUE(isLegalRefName("1337")); ASSERT_TRUE(isLegalRefName("foo.baz")); diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index a3652e522..215418522 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include @@ -1323,63 +1325,33 @@ GitRepo::WorkdirInfo GitRepo::getCachedWorkdirInfo(const std::filesystem::path & return workdirInfo; } -/** - * Checks that the git reference is valid and normalizes slash '/' sequences. - * - * Accepts shorthand references (one-level refnames are allowed). - */ -bool isValidRefNameAllowNormalizations(const std::string & refName) -{ - /* Unfortunately libgit2 doesn't expose the limit in headers, but its internal - limit is also 1024. */ - std::array normalizedRefBuffer; - - /* It would be nice to have a better API like git_reference_name_is_valid, but - * with GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND flag. libgit2 uses it internally - * but doesn't expose it in public headers [1]. - * [1]: - * https://github.com/libgit2/libgit2/blob/9d5f1bacc23594c2ba324c8f0d41b88bf0e9ef04/src/libgit2/refs.c#L1362-L1365 - */ - - auto res = git_reference_normalize_name( - normalizedRefBuffer.data(), - normalizedRefBuffer.size(), - refName.c_str(), - GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND); - - return res == 0; -} - bool isLegalRefName(const std::string & refName) { initLibGit2(); - /* Since `git_reference_normalize_name` is the best API libgit2 has for verifying - * reference names with shorthands (see comment in normalizeRefName), we need to - * ensure that exceptions to the validity checks imposed by normalization [1] are checked - * explicitly. - * [1]: https://git-scm.com/docs/git-check-ref-format#Documentation/git-check-ref-format.txt---normalize - */ - /* Check for cases that don't get rejected by libgit2. * FIXME: libgit2 should reject this. */ if (refName == "@") return false; - /* Leading slashes and consecutive slashes are stripped during normalizatiton. */ - if (refName.starts_with('/') || refName.find("//") != refName.npos) - return false; - - /* Refer to libgit2. */ - if (!isValidRefNameAllowNormalizations(refName)) - return false; - /* libgit2 doesn't barf on DEL symbol. * FIXME: libgit2 should reject this. */ if (refName.find('\177') != refName.npos) return false; - return true; + for (auto * func : { + git_reference_name_is_valid, + git_branch_name_is_valid, + git_tag_name_is_valid, + }) { + int valid = 0; + if (func(&valid, refName.c_str())) + throw Error("checking git reference '%s': %s", refName, git_error_last()->message); + if (valid) + return true; + } + + return false; } } // namespace nix diff --git a/src/libfetchers/include/nix/fetchers/git-utils.hh b/src/libfetchers/include/nix/fetchers/git-utils.hh index 07b985541..8357ce4cd 100644 --- a/src/libfetchers/include/nix/fetchers/git-utils.hh +++ b/src/libfetchers/include/nix/fetchers/git-utils.hh @@ -158,9 +158,12 @@ struct Setter }; /** - * Checks that the git reference is valid and normalized. + * Checks that the string can be a valid git reference, branch or tag name. + * Accepts shorthand references (one-level refnames are allowed), pseudorefs + * like `HEAD`. * - * Accepts shorthand references (one-level refnames are allowed). + * @note This is a coarse test to make sure that the refname is at least something + * that Git can make sense of. */ bool isLegalRefName(const std::string & refName); diff --git a/tests/functional/fetchGitRefs.sh b/tests/functional/fetchGitRefs.sh index 288b26591..a7d1a2a29 100755 --- a/tests/functional/fetchGitRefs.sh +++ b/tests/functional/fetchGitRefs.sh @@ -59,6 +59,9 @@ invalid_ref() { } +valid_ref 'A/b' +valid_ref 'AaA/b' +valid_ref 'FOO/BAR/BAZ' valid_ref 'foox' valid_ref '1337' valid_ref 'foo.baz'