Skip to content

GitInspectionRepository::snapshot reads HEAD/index/objects by path, not by pinned handle #426

Description

@BunsDev

What is wrong

Found in independent review of the #424 fix (PR #421). git_dir_for_worktree/git_repository_paths now classify and read the .git marker safely (symlink-metadata-based classification, hardened bounded reads for the linked-worktree file), but the directory identity they return is not pinned by handle. GitInspectionRepository::snapshot then does:

let (work_tree, actual_git_dir) = git_repository_paths(Path::new(root))?;
let common_dir = git_common_dir(&actual_git_dir)?;
let index = work_tree.as_ref().map(|_| actual_git_dir.join("index"));
let alternate_objects = common_dir.join("objects");
...
let actual_head = std::fs::read_to_string(actual_git_dir.join("HEAD"))
    .map_err(|e| format!("snapshot Git HEAD: {e}"))?;

HEAD is read with a plain, symlink-following, unbounded read_to_string from a path, not through GitMetadataDirectory/read_bounded_git_metadata_file. If a hostile repository swaps its .git directory (or objects/index) for a symlink between the classification in git_dir_for_worktree and these later reads, the path-based reads can still be redirected -- the same TOCTOU class the module's header describes defending against ("a .git directory can be swapped for a symlink ... between one command and the next, so directory identity is verified by handle rather than by path"), except this particular read path is not.

Scope

This gap is pre-existing and identical before and after the #424 fix -- git_dir_for_worktree's directory-marker branch has always returned a path without opening/pinning a handle, and snapshot()'s HEAD/index/objects path construction and reads have always been path-based. It is not introduced by the #424 fix, which only hardened .git marker classification and the linked-worktree marker file read.

Suggested direction

Pin the git/common directory with a no-follow handle (the GitMetadataDirectory pattern already used elsewhere in this module) at the point git_repository_paths resolves it, and thread that handle (or a validated identity check immediately before each read) through to the HEAD, index, and objects reads in snapshot(), rather than re-deriving plain paths.

Severity

Filed at the same trust boundary as #424: reachable from git_log/inspection commands with a caller-chosen, untrusted repository root. Exploitation requires winning a narrow race between classification and read, which is why this is filed separately rather than blocking #424's narrower, already-tested fix.

Related

#424 (fixed in #421). Found during independent review of that fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions