Skip to content

Operator precedence bug in git_repository_foreach_worktree #7216

@akrivka

Description

@akrivka

Operator precedence bug in git_repository_foreach_worktree

Affected versions: libgit2 <= 1.9.2 (at least; may be present in other versions)
File: src/libgit2/repository.c, function git_repository_foreach_worktree

The bug

Line 3163 has a missing pair of parentheses:

// BUGGY: '<' has higher precedence than '=', so error is assigned 0 or 1
if ((error = git_worktree_lookup(&worktree, repo, worktrees.strings[i]) < 0) ||
    (error = git_repository_open_from_worktree(&worktree_repo, worktree)) < 0) {
    if (error != GIT_ENOTFOUND)
        goto out;
    error = 0;
    continue;
}

Because < binds tighter than =, the first operand is parsed as:

(error = (git_worktree_lookup(...) < 0))

instead of the intended:

((error = git_worktree_lookup(...)) < 0)

When git_worktree_lookup fails (returns any negative value), the comparison negative < 0 evaluates to 1, so error is assigned 1 — not the actual error code. The recovery check if (error != GIT_ENOTFOUND) then compares 1 != -3, which is always true, so execution jumps to goto out with error = 1.

Note: the second operand (git_repository_open_from_worktree) is correctly parenthesized and does not have this issue.

There is also a similar bug on line 3148 where:

(error = cb(worktree_repo, payload) != 0)

assigns the boolean result of != 0 rather than the callback’s return value, which has similar consequences when the callback returns a negative error code.

Impact

git_repository_foreach_worktree is used by git_branch_is_checked_out, which returns:

return git_repository_foreach_worktree(..., branch_is_checked_out, ...) == 1;

When the bug causes foreach_worktree to return 1, git_branch_is_checked_out reports the branch as checked out — even if no worktree actually has it checked out. This is a false positive.

This directly breaks git_worktree_add. When called without an explicit ref, git_worktree_add creates a new branch and then calls git_branch_is_checked_out on that just-created branch. The false positive causes it to fail with:

reference refs/heads/<name> is already checked out

git_branch_delete is also affected, as it calls git_branch_is_checked_out and refuses to delete branches it believes are checked out.

Trigger condition

Any stale or corrupted worktree entry in .git/worktrees/ that causes git_worktree_lookup to fail with a non-GIT_ENOTFOUND error. This can happen when:

  • a worktree directory was removed but metadata in .git/worktrees/<name>/ was not pruned
  • metadata files (commondir, gitdir, HEAD) exist but contain invalid/empty content
  • any condition where the entry passes is_worktree_dir checks (used by git_worktree_list) but fails during full parse in open_worktree_dir (used by git_worktree_lookup)

Fix

The correct code should assign return values before comparison:

if (((error = git_worktree_lookup(&worktree, repo, worktrees.strings[i])) < 0) ||
    ((error = git_repository_open_from_worktree(&worktree_repo, worktree)) < 0)) {

And:

if ((error = git_repository_open(&worktree_repo, repo->commondir)) < 0 ||
    ((error = cb(worktree_repo, payload)) != 0))

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions