refs: validate the name passed to git_reference_remove - #7385
Open
ethanstoner wants to merge 1 commit into
Open
ethanstoner wants to merge 1 commit into
ethanstoner wants to merge 1 commit into
Conversation
`git_reference_remove` handed its `name` argument straight to `git_refdb_delete` without ever checking that it is a reference name. The filesystem backend resolves a reference name relative to the repository directory and only checks that the result is a valid filesystem path, so any name that happens to name a file inside the repository directory was removed. Calls with `config`, `description` or `index` therefore succeeded and deleted those control files, and `HEAD` was removed as well -- even though the object-based `git_reference_delete` explicitly refuses to delete `HEAD`. Validate the name with `git_reference__normalize_name` and reject `HEAD`, mirroring what `git_reference_delete` already does. One-level names remain removable, since `GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL` accepts them; it constrains them to upper-case letters and underscores, which excludes the lower-case control file names. Note that this only closes the one-level case. A name with more than one segment that does not live under `refs/` -- `logs/HEAD`, `info/exclude`, `objects/info/packs` -- is a well-formed reference name and still reaches the backend. Restricting those requires deciding at the backend level which names outside `refs/` are references, which is left alone here.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
git_reference_remove()never validated itsnameargument, so it could deletefiles from the repository control directory. This validates the name the same way
git_reference_delete()already does and rejectsHEAD.Fixes #7376
The problem
git_reference_remove()passednamestraight through:The filesystem backend's
loose_lock()(src/libgit2/refdb_fs.c) only checksthat the name is a valid filesystem path:
so the name is joined onto the repository directory and
loose_delete()unlinkswhatever is there.
git_reference_delete()never had this problem because itoperates on an already-looked-up
git_referenceand explicitly refusesHEAD;the by-name path had neither check.
Reproduced before the change
Against
mainat 0551dfd, on a freshgit_repository_init()repo (macOS,clang, files backend):
Each call returned success and the file was gone. After the change:
The change
src/libgit2/refs.c— validate, then rejectHEAD, before touching the refdb:GIT_REFERENCE_FORMAT_ALLOW_ONELEVELis the flag the create and lookup pathsalready use (
reference_normalize_for_repo), so this does not makeremovestricter than the rest of the reference API.
is_valid_normalized_name()constrains one-level names to
[A-Z_]+(not starting or ending with_), whichis what excludes
config,descriptionandindex;HEADpasses that test,hence the explicit guard, worded and returning the same way
git_reference_delete()does.Passing
NULLas the output buffer is the supported "validate only" mode ofgit_reference__normalize_name(normalize = (buf != NULL)), so nothing isallocated on this path.
Edge cases a reviewer will probably ask about
ORIG_HEAD,FETCH_HEAD,MERGE_HEAD,CHERRY_PICK_HEADall satisfy[A-Z_]+. There is a new testthat creates and removes
ORIG_HEAD.@still works.is_valid_normalized_name()special-cases a single@as shorthand for
HEAD; that is unchanged, and it is not caught by theHEADguard (
strcmpagainst the literal name, as ingit_reference_delete).refs.c, abovegit_refdb_delete, soit applies to packed refs, loose refs and the reftable backend identically. No
backend was touched.
NULLname now returns an error instead of reaching the backend:git_reference__normalize_namestarts withGIT_ASSERT_ARG(name).path through this API now gets
GIT_EINVALIDSPECinstead of0. That is thepoint of the fix, but it is an observable change, so the public doc comment in
include/git2/refs.hnow states thatnamemust be a well-formed referencename, that
HEADcannot be removed, and thatGIT_EINVALIDSPECis possible.No error code that was previously returned has changed meaning.
What this does NOT fix — please read
Refname syntax is not the same thing as "lives under
refs/". A name with morethan one segment that is outside
refs/is a perfectly well-formed referencename, so it still passes normalization and still reaches the loose backend.
Verified with the patch applied:
So this PR closes the one-level cases from the issue (
config,description,index,HEAD) and leaves the multi-level cases (includinglogs/HEAD, whichthe issue also lists) open. I did not want to imply otherwise.
Closing that gap means deciding, at the backend or at
refs.c, which namesoutside
refs/count as references — core git uses an explicit root-refallowlist (
HEAD,ORIG_HEAD,FETCH_HEAD,MERGE_HEAD,CHERRY_PICK_HEAD,REVERT_HEAD,REBASE_HEAD,AUTO_MERGE,BISECT_EXPECT_REV). libgit2 hassomething adjacent in
git_reference__is_pseudoref(), but it lists onlyMERGE_HEADandFETCH_HEADand is used for lookup. Applying a policy likethat would also have to apply to
git_reference_create(),git_reference_lookup()andgit_reference_rename()to stay coherent, which isa larger API decision than a bug fix should make unilaterally. Happy to do it in
this PR or a follow-up if you tell me which shape you want.
Validation
Built on macOS 15 / arm64, clang, Ninja,
-DREGEX_BACKEND=regcomp_l,-DUSE_HTTPS=SecureTransport.refs::delete./libgit2_tests -srefs::deleterefssuites./libgit2_tests -srefsrefs, reftableCLAR_REF_FORMAT=reftable ./libgit2_tests -srefsctest -R '^offline$'CLAR_REF_FORMAT=reftable ctest -R '^offline$'ctest -R '^util$'script/api-docs/api-generator.js --validate-only --strict --deprecate-hard .Before the change, the two new negative tests failed as expected:
Not run locally, so CI will be the first to cover them: the Linux/Windows
matrix, the sanitizer builds (ASan/MSan/UBSan/TSan), the leak checkers
(
valgrind/leaks/win32), the-DENABLE_WERROR=ON -DDEPRECATE_HARD=ONbuild,the online/network suites, and the invasive suite. The change adds no
allocations and no new branches in a hot path, so I do not expect sanitizer
movement, but I have not proven that.
No
docs/changelog.mdentry — that file looks like it is updated per releaserather than per PR, tell me if you want one.
Note on disclosure
#7376 is already a public issue, so I am following up on it publicly. If you
consider this a security issue that should have gone to security@libgit2.com per
SECURITY.mdinstead, say so and I will close this and resend privately.Checklist
include/git2/refs.h)Generated with Claude Code on behalf of @ethanstoner.