diff --git a/codex-rs/core/src/git_info_tests.rs b/codex-rs/core/src/git_info_tests.rs index 946779d06b..89d14c4c5c 100644 --- a/codex-rs/core/src/git_info_tests.rs +++ b/codex-rs/core/src/git_info_tests.rs @@ -615,6 +615,11 @@ async fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git format!("gitdir: {}\n", worktree_git_dir.display()), ) .unwrap(); + std::fs::write( + worktree_git_dir.join("gitdir"), + format!("{}\n", worktree_root.join(".git").display()), + ) + .unwrap(); let expected = repo_root.abs(); let worktree_root = worktree_root.abs(); @@ -629,6 +634,30 @@ async fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git ); } +#[tokio::test] +async fn resolve_root_git_project_for_trust_rejects_forged_worktree_pointer() { + let tmp = TempDir::new().expect("tempdir"); + let trusted_repo = tmp.path().join("trusted"); + let forged_repo = tmp.path().join("forged"); + let forged_git_dir = trusted_repo + .join(".git") + .join("worktrees") + .join("feature-x"); + std::fs::create_dir_all(&forged_git_dir).unwrap(); + std::fs::create_dir_all(&forged_repo).unwrap(); + std::fs::write( + forged_repo.join(".git"), + format!("gitdir: {}\n", forged_git_dir.display()), + ) + .unwrap(); + + assert!( + resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &forged_repo.abs()) + .await + .is_none() + ); +} + #[tokio::test] async fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() { let tmp = TempDir::new().expect("tempdir"); diff --git a/codex-rs/git-utils/src/info.rs b/codex-rs/git-utils/src/info.rs index a188d9a876..bbe524c4ed 100644 --- a/codex-rs/git-utils/src/info.rs +++ b/codex-rs/git-utils/src/info.rs @@ -764,11 +764,41 @@ pub async fn resolve_root_git_project_for_trust( } let git_dir_path = AbsolutePathBuf::resolve_path_against_base(git_dir_rel, repo_root.as_path()); + if !fs + .get_metadata(&git_dir_path, /*sandbox*/ None) + .await + .ok()? + .is_directory + { + return None; + } + let worktrees_dir = git_dir_path.parent()?; if worktrees_dir.as_path().file_name() != Some(OsStr::new("worktrees")) { return None; } + // A linked worktree is valid only when its gitdir points back to this checkout's .git file. + let git_dir_backref = fs + .read_file_text(&git_dir_path.join("gitdir"), /*sandbox*/ None) + .await + .ok()?; + let git_dir_backref = git_dir_backref.trim(); + if git_dir_backref.is_empty() { + return None; + } + + let git_dir_backref_path = + AbsolutePathBuf::resolve_path_against_base(git_dir_backref, git_dir_path.as_path()); + if fs + .canonicalize(&git_dir_backref_path, /*sandbox*/ None) + .await + .ok()? + != fs.canonicalize(&dot_git, /*sandbox*/ None).await.ok()? + { + return None; + } + let common_dir = worktrees_dir.parent()?; common_dir.parent() }