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.
What is wrong
Found in independent review of the #424 fix (PR #421).
git_dir_for_worktree/git_repository_pathsnow classify and read the.gitmarker 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::snapshotthen does:HEADis read with a plain, symlink-following, unboundedread_to_stringfrom a path, not throughGitMetadataDirectory/read_bounded_git_metadata_file. If a hostile repository swaps its.gitdirectory (orobjects/index) for a symlink between the classification ingit_dir_for_worktreeand these later reads, the path-based reads can still be redirected -- the same TOCTOU class the module's header describes defending against ("a.gitdirectory 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, andsnapshot()'s HEAD/index/objects path construction and reads have always been path-based. It is not introduced by the #424 fix, which only hardened.gitmarker classification and the linked-worktree marker file read.Suggested direction
Pin the git/common directory with a no-follow handle (the
GitMetadataDirectorypattern already used elsewhere in this module) at the pointgit_repository_pathsresolves it, and thread that handle (or a validated identity check immediately before each read) through to theHEAD,index, andobjectsreads insnapshot(), 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.