git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH 2/2] refs_resolve_ref_unsafe: handle d/f conflicts for writes

From
Michael Haggerty <mhagger@alum.mit.edu>
Date
Oct 6, 2017, 17:09 UTC
Message-ID
<38c17fdc-7a3b-d166-1abe-afe64fc823c5@alum.mit.edu>
In-Reply-To
<20171006144217.y6oxux26hh2fb7og@sigill.intra.peff.net>
On 10/06/2017 04:42 PM, Jeff King wrote:
Show 87 quoted lines
> If our call to refs_read_raw_ref() fails, we check errno to
> see if the ref is simply missing, or if we encountered a
> more serious error. If it's just missing, then in "write"
> mode (i.e., when RESOLVE_REFS_READING is not set), this is
> perfectly fine.
> 
> However, checking for ENOENT isn't sufficient to catch all
> missing-ref cases. In the filesystem backend, we may also
> see EISDIR when we try to resolve "a" and "a/b" exists.
> Likewise, we may see ENOTDIR if we try to resolve "a/b" and
> "a" exists. In both of those cases, we know that our
> resolved ref doesn't exist, but we return an error (rather
> than reporting the refname and returning a null sha1).
> 
> This has been broken for a long time, but nobody really
> noticed because the next step after resolving without the
> READING flag is usually to lock the ref and write it. But in
> both of those cases, the write will fail with the same
> errno due to the directory/file conflict.
> 
> There are two cases where we can notice this, though:
> 
>   1. If we try to write "a" and there's a leftover directory
>      already at "a", even though there is no ref "a/b". The
>      actual write is smart enough to move the empty "a" out
>      of the way.
> 
>      This is reasonably rare, if only because the writing
>      code has to do an independent resolution before trying
>      its write (because the actual update_ref() code handles
>      this case fine). The notes-merge code does this, and
>      before the fix in the prior commit t3308 erroneously
>      expected this case to fail.
> 
>   2. When resolving symbolic refs, we typically do not use
>      the READING flag because we want to resolve even
>      symrefs that point to unborn refs. Even if those unborn
>      refs could not actually be written because of d/f
>      conflicts with existing refs.
> 
>      You can see this by asking "git symbolic-ref" to report
>      the target of a symref pointing past a d/f conflict.
> 
> We can fix the problem by recognizing the other "missing"
> errnos and treating them like ENOENT. This should be safe to
> do even for callers who are then going to actually write the
> ref, because the actual writing process will fail if the d/f
> conflict is a real one (and t1404 checks these cases).
> 
> Arguably this should be the responsibility of the
> files-backend to normalize all "missing ref" errors into
> ENOENT (since something like EISDIR may not be meaningful at
> all to a database backend). However other callers of
> refs_read_raw_ref() may actually care about the distinction;
> putting this into resolve_ref() is the minimal fix for now.
> 
> The new tests in t1401 use git-symbolic-ref, which is the
> most direct way to check the resolution by itself.
> Interestingly we actually had a test that setup this case
> already, but we only used it to verify that the funny state
> could be overwritten, not that it could be resolved.
> 
> We also add a new test in t3200, as "branch -m" was the
> original motivation for looking into this. What happens is
> this:
> 
>   0. HEAD is pointing to branch "a"
> 
>   1. The user asks to rename "a" to "a/b".
> 
>   2. We create "a/b" and delete "a".
> 
>   3. We then try to update any worktree HEADs that point to
>      the renamed ref (including the main repo HEAD). To do
>      that, we have to resolve each HEAD. But now our HEAD is
>      pointing at "a", and we get EISDIR due to the loose
>      "a/b". As a result, we think there is no HEAD, and we
>      do not update it. It now points to the bogus "a".
> 
> Interestingly this case used to work, but only accidentally.
> Before 31824d180d (branch: fix branch renaming not updating
> HEADs correctly, 2017-08-24), we'd update any HEAD which we
> couldn't resolve. That was wrong, but it papered over the
> fact that we were incorrectly failing to resolve HEAD.
> 
> So while the bug demonstrated by the git-symbolic-ref is
> quite old, the regression to "branch -m" is recent.
Thanks for your detailed investigation and analysis and for the new tests.
This makes sense to me at the level of fixing the bug.

I do have one twinge of uneasiness at a deeper level, that I haven't had time to check...

Does this patch make it easier to *set* HEAD to an unborn branch that d/f conflicts with an existing reference? If so, that might be a slightly worse UI for users. I'd rather learn about such a problem when setting HEAD (when I am thinking about the new branch name and am in the frame of mind to solve the problem) rather than later, when I try to commit to the new branch.

Even if so, that wouldn't be a problem with this patch per se, but rather a possible accidental side-effect of fixing the bug.

Michael
> [...]
Previous: Jeff KingNext: Jeff King
Message 10 of 14 in “Regression in 'git branch -m'?”
  1. Andreas KreyOct 5, 2017
  2. Jeff KingOct 5, 2017
  3. Jeff KingOct 6, 2017
  4. Jeff KingOct 6, 2017
  5. Junio C HamanoOct 6, 2017
  6. Jeff KingOct 6, 2017
  7. Jeff KingOct 6, 2017
  8. 1/2 t3308: create a real ref directory/file conflictJeff King, Oct 6, 2017
  9. 2/2 refs_resolve_ref_unsafe: handle d/f conflicts for writesJeff King, Oct 6, 2017
  10. Michael HaggertyOct 6, 2017
  11. Jeff KingOct 6, 2017
  12. Michael HaggertyOct 7, 2017
  13. Michael HaggertyNov 5, 2017
  14. Junio C HamanoOct 7, 2017

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.