use super::*; use pretty_assertions::assert_eq; use std::process::Command; use crate::safe_git::DISABLED_HOOKS_PATH; #[cfg(unix)] fn write_executable(path: &Path, body: &str) { use std::os::unix::fs::PermissionsExt; std::fs::write(path, body).expect("write executable"); let mut permissions = std::fs::metadata(path) .expect("executable metadata") .permissions(); permissions.set_mode(0o755); std::fs::set_permissions(path, permissions).expect("set executable permissions"); } fn run_git(cwd: &Path, args: &[&str]) { let mut command = Command::new("git"); isolate_git_command_environment(&mut command); let status = command .args([ "-c", &format!("core.hooksPath={DISABLED_HOOKS_PATH}"), "-c", "core.fsmonitor=false", ]) .args(args) .current_dir(cwd) .status() .expect("run real Git"); assert!(status.success(), "git {args:?} failed"); } fn run_git_stdout(cwd: &Path, args: &[&str]) -> String { let mut command = Command::new("git"); isolate_git_command_environment(&mut command); let output = command .args(args) .current_dir(cwd) .output() .expect("run real Git"); assert!( output.status.success(), "git {args:?} failed: {}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout) .expect("UTF-8 Git output") .trim() .to_string() } fn commit_all(cwd: &Path, message: &str) { run_git( cwd, &[ "-c", "user.name=Codex Test", "-c", "user.email=codex@example.com", "-c", "commit.gpgSign=false", "commit", "-qam", message, ], ); } fn write_git_candidate(directory: &Path) { std::fs::create_dir_all(directory).expect("create candidate directory"); let candidate = directory.join(git_executable_name()); #[cfg(windows)] { let mut pe = [0_u8; 68]; pe[..2].copy_from_slice(b"MZ"); pe[60..64].copy_from_slice(&64_u32.to_le_bytes()); pe[64..].copy_from_slice(b"PE\0\0"); std::fs::write(candidate, pe).expect("write native PE fixture"); } #[cfg(not(windows))] std::fs::copy(native_git_fixture(), candidate).expect("copy native Git fixture"); } fn write_runnable_git_candidate(directory: &Path) { #[cfg(windows)] { std::fs::create_dir_all(directory.parent().expect("candidate parent")) .expect("create candidate parent"); create_junction(directory, &native_git_search_directory()); } #[cfg(not(windows))] write_git_candidate(directory); } #[cfg(windows)] fn native_git_search_directory() -> PathBuf { let path = std::env::var_os("PATH").expect("PATH"); for directory in std::env::split_paths(&path) { let candidate = directory.join(git_executable_name()); if let Ok(candidate) = std::fs::canonicalize(candidate) && crate::git_executable::is_native_executable_file(&candidate) { return directory; } } panic!("no native Git directory in PATH") } fn native_git_fixture() -> PathBuf { let path = std::env::var_os("PATH").expect("PATH"); for directory in std::env::split_paths(&path) { let candidate = directory.join(git_executable_name()); if let Ok(candidate) = std::fs::canonicalize(candidate) && crate::git_executable::is_native_executable_file(&candidate) { return candidate; } } panic!("no native Git fixture in PATH") } #[cfg(windows)] fn create_junction(path: &Path, target: &Path) { // Bazel's GNU Windows runner can surface temporary paths with `/` // separators. `mklink` treats those separators as option prefixes, so // pass native separators to the cmd.exe built-in. let path = path.as_os_str().to_string_lossy().replace('/', "\\"); let target = target.as_os_str().to_string_lossy().replace('/', "\\"); let output = Command::new("cmd.exe") .args(["/D", "/C", "mklink", "/J"]) .arg(path) .arg(target) .output() .expect("create junction"); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "mklink failed: stdout={stdout} stderr={stderr}" ); } fn locations_for_root(root: &Path) -> RepositoryAuthority { let mut roots = vec![root.to_path_buf()]; let canonical = std::fs::canonicalize(root).expect("canonical root"); if !roots.contains(&canonical) { roots.push(canonical); } RepositoryAuthority::from_test_locations(roots.clone(), roots, Vec::new()) .expect("test repository authority") } fn config_authority_for_root(root: &Path) -> RepositoryAuthority { let mut roots = vec![root.to_path_buf()]; let canonical = std::fs::canonicalize(root).expect("canonical root"); if !roots.contains(&canonical) { roots.push(canonical); } RepositoryAuthority::from_test_locations(roots.clone(), roots, vec![root.join(".git")]) .expect("config repository authority") } fn raw_parent_traversal(root: &Path, sibling: &str) -> PathBuf { let separator = std::path::MAIN_SEPARATOR.to_string(); let mut path = root.as_os_str().to_os_string(); path.push(&separator); path.push(".."); path.push(&separator); path.push(sibling); path.into() } fn path_text(path: &Path) -> &str { path.to_str().expect("UTF-8 fixture path") } fn tempdir_for_native_git() -> tempfile::TempDir { #[cfg(windows)] { // Git for Windows rejects the `\\?\` spelling returned when the // process temp directory is canonicalized. tempfile::tempdir().expect("fixture") } #[cfg(not(windows))] { let temp_base = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp dir"); tempfile::tempdir_in(temp_base).expect("fixture") } } fn git_config_path(path: &Path) -> String { let path = path.to_string_lossy(); #[cfg(windows)] let path = path.replace('\\', "/"); #[cfg(not(windows))] let path = path.into_owned(); format!("\"{}\"", path.replace('\\', "\\\\").replace('"', "\\\"")) } struct OverlappingRegisteredRoute { main: PathBuf, nested: PathBuf, linked: PathBuf, #[cfg(unix)] nested_admin: PathBuf, linked_admin: PathBuf, registry_marker: PathBuf, raw_marker: PathBuf, } fn overlapping_registered_route(fixture: &Path) -> OverlappingRegisteredRoute { let main = fixture.join("main"); let nested = main.join("nested"); let linked = main.join("linked"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&nested)]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); #[cfg(unix)] let nested_admin = PathBuf::from(run_git_stdout( &nested, &["rev-parse", "--absolute-git-dir"], )); let linked_admin = PathBuf::from(run_git_stdout( &linked, &["rev-parse", "--absolute-git-dir"], )); let registry_marker = linked_admin.join("gitdir"); let raw_marker = nested.join("..").join("linked/.git"); std::fs::write(®istry_marker, format!("{}\n", raw_marker.display())) .expect("write overlapping registry route"); OverlappingRegisteredRoute { main, nested, linked, #[cfg(unix)] nested_admin, linked_admin, registry_marker, raw_marker, } } struct MetadataAliasRegisteredRoute { main: PathBuf, linked: PathBuf, linked_admin: PathBuf, pivot: PathBuf, registry_marker: PathBuf, #[cfg(unix)] raw_marker: PathBuf, } fn metadata_alias_registered_route( fixture: &Path, main_name: &str, route_main_name: &str, ) -> MetadataAliasRegisteredRoute { let main = fixture.join(main_name); let linked = main.join("linked"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); let linked_admin = PathBuf::from(run_git_stdout( &linked, &["rev-parse", "--absolute-git-dir"], )); let pivot = main.join("pivot"); let registry_marker = linked_admin.join("gitdir"); let raw_marker = fixture .join(route_main_name) .join("pivot") .join("..") .join("linked/.git"); std::fs::write(®istry_marker, format!("{}\n", raw_marker.display())) .expect("write metadata-alias registry route"); MetadataAliasRegisteredRoute { main, linked, linked_admin, pivot, registry_marker, #[cfg(unix)] raw_marker, } } #[test] fn git_metadata_marker_parser_preserves_leading_path_space() { assert_eq!( parse_git_marker_path(b"gitdir: relative/path \r\n", b"gitdir: ") .expect("parse .git marker"), PathBuf::from(" relative/path") ); assert_eq!( parse_git_marker_path(b" relative/common \n", b"").expect("parse commondir marker"), PathBuf::from(" relative/common") ); assert!(parse_git_marker_path(b"gitdir:/missing-space\n", b"gitdir: ").is_err()); } #[test] fn config_source_authority_requires_an_absolute_raw_path() { let fixture = tempdir_for_native_git(); let root = fixture.path().join("repo"); std::fs::create_dir_all(&root).expect("create repository"); run_git(&root, &["init", "-q"]); let authority = config_authority_for_root(&root); let path = Path::new("relative/config"); let error = authority .ensure_config_source_is_not_worktree_controlled(path, "test config") .expect_err("relative config path"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); assert!(error.to_string().contains("test config"), "{error}"); assert!(error.to_string().contains("relative/config"), "{error}"); } #[test] fn config_source_authority_preserves_and_rejects_a_worktree_crossing_spelling() { let fixture = tempdir_for_native_git(); let root = fixture.path().join("repo"); let safe = fixture.path().join("safe"); std::fs::create_dir_all(root.join("nested")).expect("create repository descendant"); std::fs::create_dir_all(&safe).expect("create safe directory"); run_git(&root, &["init", "-q"]); let authority = config_authority_for_root(&root); let raw = root .join("nested") .join("..") .join("..") .join("safe/config"); let error = authority .ensure_config_source_is_not_worktree_controlled(&raw, "crossing config") .expect_err("worktree-crossing config path"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); assert!(error.to_string().contains("crossing config"), "{error}"); assert!( error.to_string().contains(&raw.display().to_string()), "{error}" ); } #[test] fn config_source_authority_allows_protected_metadata_and_unrelated_external_paths() { let fixture = tempdir_for_native_git(); let root = fixture.path().join("repo"); let external = fixture.path().join("external/config"); std::fs::create_dir_all(&root).expect("create repository"); std::fs::create_dir_all(external.parent().expect("external parent")) .expect("create external directory"); run_git(&root, &["init", "-q"]); let authority = config_authority_for_root(&root); authority .ensure_config_source_is_not_worktree_controlled( &root.join(".git/config"), "protected metadata config", ) .expect("protected metadata config"); let canonical_config = std::fs::canonicalize(root.join(".git/config")).expect("canonical metadata config"); authority .ensure_config_source_is_not_worktree_controlled( &canonical_config, "canonical protected metadata config", ) .expect("canonical protected metadata config"); authority .ensure_config_source_is_not_worktree_controlled(&external, "external config") .expect("unrelated external config"); } #[test] fn config_source_authority_rejects_an_unregistered_related_repository_root() { let fixture = tempdir_for_native_git(); let root = fixture.path().join("repo"); let alias = fixture.path().join("unregistered-related-root"); std::fs::create_dir_all(&root).expect("create repository"); std::fs::create_dir_all(&alias).expect("create related root"); run_git(&root, &["init", "-q"]); std::fs::write( alias.join(".git"), format!("gitdir: {}\n", root.join(".git").display()), ) .expect("write related metadata marker"); let authority = config_authority_for_root(&root); let path = alias.join("config"); let error = authority .ensure_config_source_is_not_worktree_controlled(&path, "related config") .expect_err("unregistered related repository config"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); assert!(error.to_string().contains("related config"), "{error}"); } fn selected_git(locations: &RepositoryAuthority, directories: &[&Path]) -> PathBuf { let search_path = std::env::join_paths(directories).expect("PATH"); select_git_executable(locations, &search_path) .expect("trusted Git") .argv0 } fn assert_unsafe_metadata_route(result: Result, marker: &Path) { match result { Err(GitReadError::UnsafeRepositoryMetadata { path, reason }) => { assert_same_affected_path(&path, marker); assert_eq!(reason, "Git metadata route crosses a repository worktree"); } other => panic!("expected unsafe repository metadata, got {other:?}"), } } fn assert_unsafe_registry_route(result: Result, marker: &Path) { match result { Err(GitReadError::UnsafeRepositoryMetadata { path, reason }) => { assert_same_affected_path(&path, marker); assert_eq!( reason, "Git worktree registry route crosses a repository worktree" ); } other => panic!("expected unsafe worktree registry route, got {other:?}"), } } #[cfg(unix)] fn assert_unsafe_registry_route_at_one_of( result: Result, markers: &[&Path], ) { match result { Err(GitReadError::UnsafeRepositoryMetadata { path, reason }) => { assert_eq!( reason, "Git worktree registry route crosses a repository worktree" ); assert!( markers .iter() .any(|marker| affected_paths_match(&path, marker)), "unexpected affected registry path: {}", path.display() ); } other => panic!("expected unsafe worktree registry route, got {other:?}"), } } fn assert_same_affected_path(actual: &Path, expected: &Path) { assert!( affected_paths_match(actual, expected), "affected paths differ: actual={} expected={}", actual.display(), expected.display() ); } fn affected_paths_match(actual: &Path, expected: &Path) -> bool { if actual.file_name() != expected.file_name() { return false; } let Some(actual_parent) = actual.parent() else { return false; }; let Some(expected_parent) = expected.parent() else { return false; }; same_file::is_same_file(actual_parent, expected_parent).unwrap_or(false) } #[test] fn git_read_error_io_kind_table_is_exhaustive() { let path = PathBuf::from("repository/.git"); for (error, expected) in [ (GitReadError::NoTrustedGit, io::ErrorKind::NotFound), ( GitReadError::NotRepository { path: path.clone() }, io::ErrorKind::NotFound, ), ( GitReadError::UnprovenPrimaryAuthority { common_dir: "repository.git".to_string(), }, io::ErrorKind::PermissionDenied, ), ( GitReadError::UnsafeRepositoryMetadata { path: path.clone(), reason: "crosses worktree".to_string(), }, io::ErrorKind::PermissionDenied, ), ( GitReadError::InvalidRepositoryMetadata { path, reason: "malformed marker".to_string(), }, io::ErrorKind::InvalidData, ), ] { assert_eq!(error.io_kind(), expected, "{error}"); assert_eq!(error.into_io_error().kind(), expected); } } #[test] fn malformed_metadata_preserves_path_reason_and_invalid_data_mapping() { let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repository"); std::fs::create_dir_all(&root).expect("create repository root"); let marker = root.join(".git"); std::fs::write(&marker, "not-a-gitdir-marker\n").expect("write malformed marker"); match GitRunner::for_cwd(&root) { Err(GitReadError::InvalidRepositoryMetadata { path, reason }) => { assert_same_affected_path(&path, &marker); assert!(reason.contains("malformed Git metadata marker"), "{reason}"); } other => panic!("expected invalid repository metadata, got {other:?}"), } let error = GitRunner::for_cwd_io(&root).expect_err("malformed metadata"); assert_eq!(error.kind(), io::ErrorKind::InvalidData); assert!( error.to_string().contains("malformed Git metadata marker"), "{error}" ); assert!(!error.to_string().contains("PATH"), "{error}"); } #[cfg(unix)] #[test] fn cyclic_metadata_route_is_invalid_not_unsafe() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repository"); let external = fixture.path().join("external"); std::fs::create_dir_all(&root).expect("create repository root"); std::fs::create_dir_all(&external).expect("create external directory"); let route = external.join("entry"); symlink("entry", &route).expect("create route cycle"); let marker = root.join(".git"); std::fs::write(&marker, format!("gitdir: {}\n", route.display())).expect("write cyclic marker"); match GitRunner::for_cwd(&root) { Err(GitReadError::InvalidRepositoryMetadata { path, reason }) => { assert_same_affected_path(&path, &marker); assert!(!reason.is_empty()); } other => panic!("expected invalid repository metadata, got {other:?}"), } assert_eq!( GitRunner::for_cwd_io(&root) .expect_err("cyclic metadata route") .kind(), io::ErrorKind::InvalidData ); } #[cfg(unix)] #[test] fn symlinked_directory_metadata_marker_is_invalid() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repository"); let external = fixture.path().join("external-metadata"); std::fs::create_dir_all(&root).expect("create repository root"); run_git(&root, &["init", "-q"]); std::fs::rename(root.join(".git"), &external).expect("move metadata directory"); symlink(&external, root.join(".git")).expect("symlink metadata marker"); match GitRunner::for_cwd(&root) { Err(GitReadError::InvalidRepositoryMetadata { path, reason }) => { assert_same_affected_path(&path, &root.join(".git")); assert_eq!(reason, "symlinked Git metadata marker"); } other => panic!("expected invalid symlinked metadata marker, got {other:?}"), } assert_eq!( GitRunner::for_cwd_io(&root) .expect_err("symlinked metadata marker") .kind(), io::ErrorKind::InvalidData ); } #[cfg(windows)] #[test] fn directory_git_junction_to_external_metadata_is_rejected() { let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repository"); let external = fixture.path().join("external-metadata"); std::fs::create_dir_all(&root).expect("create repository root"); run_git(&root, &["init", "-q"]); std::fs::rename(root.join(".git"), &external).expect("move metadata directory"); create_junction(&root.join(".git"), &external); run_git(&root, &["rev-parse", "--absolute-git-dir"]); let error = GitRunner::for_cwd(&root).expect_err("nonstandard directory metadata route"); match error { GitReadError::UnsafeRepositoryMetadata { path, reason } => { assert_same_affected_path(&path, &root.join(".git")); assert_eq!(reason, "nonstandard Git metadata directory"); } GitReadError::InvalidRepositoryMetadata { path, reason } => { assert_same_affected_path(&path, &root.join(".git")); assert_eq!(reason, "symlinked Git metadata marker"); } other => panic!("expected rejected directory metadata junction, got {other:?}"), } } #[cfg(unix)] #[test] fn resolver_skips_untrusted_path_entries_and_runs_external_candidate() { let fixture = tempfile::tempdir().expect("fixture"); let repo = fixture.path().join("repo"); let repo_bin = repo.join("bin"); let outside = fixture.path().join("outside"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&repo_bin).expect("repo bin"); std::fs::create_dir_all(&outside).expect("outside bin"); std::fs::create_dir_all(&trusted_bin).expect("trusted bin"); write_git_candidate(&repo_bin); std::os::unix::fs::symlink(repo_bin.join("git"), outside.join("git")) .expect("outside symlink into repository"); write_git_candidate(&trusted_bin); let path = std::env::join_paths([ PathBuf::from("relative"), repo_bin, outside, trusted_bin.clone(), ]) .expect("PATH"); let locations = locations_for_root(&repo); let runner = GitRunner::from_search_path(locations, &path).expect("trusted Git"); assert_eq!(runner.argv0, trusted_bin.join("git")); let mut command = runner.command(); command.arg("--version"); let output = runner.output(command).expect("run trusted Git"); assert!(output.status.success()); assert!(output.stdout.starts_with(b"git version ")); } #[cfg(unix)] #[test] fn resolver_skips_env_absolute_and_no_shebang_git_scripts() { let temp_base = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp dir"); let fixture = tempfile::tempdir_in(temp_base).expect("fixture"); let repo = fixture.path().join("repo"); let trusted = fixture.path().join("trusted"); std::fs::create_dir_all(&repo).expect("create repository"); write_git_candidate(&trusted); let mut script_dirs = Vec::new(); for (name, prefix) in [ ("env-shebang", "#!/usr/bin/env sh\n"), ("absolute-shebang", "#!/bin/sh\n"), ("no-shebang", ""), ] { let directory = fixture.path().join(name); let marker = fixture.path().join(format!("{name}-ran")); std::fs::create_dir_all(&directory).expect("create script directory"); write_executable( &directory.join("git"), &format!("{prefix}touch '{}'\n", marker.display()), ); script_dirs.push((directory, marker)); } let locations = locations_for_root(&repo); let mut path_entries = script_dirs .iter() .map(|(directory, _)| directory.as_path()) .collect::>(); let scripts_only = std::env::join_paths(&path_entries).expect("scripts-only PATH"); let error = match select_git_executable(&locations, &scripts_only) { Ok(_) => panic!("selected a non-native Git wrapper"), Err(error) => error, }; assert_eq!(error, GitReadError::NoTrustedGit); let message = error.to_string(); assert!(message.contains("native Git"), "{message}"); assert!( message.contains("script-based and non-native Git wrappers"), "{message}" ); assert!(message.contains("outside the repository"), "{message}"); assert!(message.contains("PATH"), "{message}"); assert_eq!(error.io_kind(), io::ErrorKind::NotFound); for (_, marker) in &script_dirs { assert!( !marker.exists(), "non-native Git script executed: {marker:?}" ); } path_entries.push(&trusted); let path = std::env::join_paths(path_entries).expect("PATH"); let runner = GitRunner::from_search_path(locations, &path).expect("native Git fallback"); assert_eq!(runner.argv0, trusted.join("git")); let mut command = runner.command(); command.arg("--version"); assert!( runner .output(command) .expect("run native Git") .status .success() ); for (_, marker) in script_dirs { assert!( !marker.exists(), "non-native Git script executed: {marker:?}" ); } } #[cfg(unix)] #[test] fn selected_git_uses_sanitized_path_and_strips_loader_injection_environment() { let temp_base = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp dir"); let fixture = tempfile::tempdir_in(temp_base).expect("fixture"); let repo = fixture.path().join("repo"); let unsafe_bin = repo.join("bin"); let trusted = fixture.path().join("trusted"); std::fs::create_dir_all(&unsafe_bin).expect("create unsafe bin"); std::fs::create_dir_all(&trusted).expect("create trusted bin"); std::fs::copy( std::env::current_exe().expect("current test executable"), trusted.join("git"), ) .expect("copy native environment probe fixture"); let locations = locations_for_root(&repo); let path = std::env::join_paths([&unsafe_bin, &trusted]).expect("PATH"); let runner = GitRunner::from_search_path(locations, &path).expect("trusted native env"); let mut command = runner.command(); let injected = repo.join("inject"); command .arg("git_command::tests::native_environment_probe_child") .arg("--exact") .arg("--nocapture") .env("CODEX_GIT_ENVIRONMENT_PROBE_CHILD", "1") .env( "CODEX_GIT_EXPECTED_SAFE_PATH", std::fs::canonicalize(&trusted).expect("canonical trusted bin"), ); for name in [ "DYLD_INSERT_LIBRARIES", "LD_PRELOAD", "LD_AUDIT", "LIBPATH", "SHLIB_PATH", "GCONV_PATH", "NIX_LD", "NIX_LD_LIBRARY_PATH", "CORECLR_ENABLE_PROFILING", "COR_ENABLE_PROFILING", "DOTNET_STARTUP_HOOKS", ] { command.env(name, &injected); } command.env("PATH", &path); let output = runner.output(command).expect("run sanitized native env"); assert!( output.status.success(), "native env failed with {}: stdout={} stderr={}", output.status, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr), ); } #[cfg(unix)] #[test] fn native_environment_probe_child() { if std::env::var_os("CODEX_GIT_ENVIRONMENT_PROBE_CHILD").is_none() { return; } for name in [ "DYLD_INSERT_LIBRARIES", "LD_PRELOAD", "LD_AUDIT", "LIBPATH", "SHLIB_PATH", "GCONV_PATH", "NIX_LD", "NIX_LD_LIBRARY_PATH", "CORECLR_ENABLE_PROFILING", "COR_ENABLE_PROFILING", "DOTNET_STARTUP_HOOKS", ] { assert!(std::env::var_os(name).is_none(), "{name} survived"); } assert_eq!( std::env::var_os("PATH").expect("sanitized PATH"), std::env::var_os("CODEX_GIT_EXPECTED_SAFE_PATH").expect("expected safe PATH") ); } #[test] fn command_for_relative_cwd_uses_original_process_directory() { let current = std::fs::canonicalize(std::env::current_dir().expect("current dir")) .expect("canonical current dir"); let fixture = tempfile::tempdir_in(¤t).expect("fixture"); let repo = fixture.path().join("repo"); std::fs::create_dir_all(&repo).expect("create repository"); run_git(&repo, &["init", "-q"]); let relative = repo.strip_prefix(¤t).expect("relative repository"); let locations = repository_authority_for_cwd(&repo).expect("untrusted locations"); let path = std::env::var_os("PATH").expect("PATH"); let runner = GitRunner::from_search_path(locations, &path).expect("trusted Git"); let mut command = runner .command_for_cwd(relative) .expect("relative command cwd"); command.args(["rev-parse", "--show-toplevel"]); crate::repository_authority::reset_bounded_marker_read_count(); let output = runner.output(command).expect("run Git from relative cwd"); assert!(output.status.success()); assert_eq!( std::fs::canonicalize(String::from_utf8_lossy(&output.stdout).trim()) .expect("canonical Git root"), std::fs::canonicalize(&repo).expect("canonical expected root") ); assert_eq!( crate::repository_authority::bounded_marker_read_count(), 0, "standard repository should not reread marker bodies" ); } #[cfg(unix)] #[test] fn selected_runner_executes_pinned_target_after_path_hop_is_retargeted() { let temp_base = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp dir"); let fixture = tempfile::tempdir_in(temp_base).expect("fixture"); let repo = fixture.path().join("repo"); let switch = fixture.path().join("switch"); let trusted = fixture.path().join("trusted"); let attacker = fixture.path().join("attacker"); let entry = fixture.path().join("entry"); std::fs::create_dir_all(&repo).expect("create repository"); std::fs::create_dir_all(&trusted).expect("create trusted target"); std::fs::create_dir_all(&attacker).expect("create attacker target"); std::fs::create_dir_all(&entry).expect("create PATH entry"); std::fs::copy("/usr/bin/true", trusted.join("git")).expect("copy trusted native target"); std::fs::copy("/usr/bin/false", attacker.join("git")).expect("copy attacker native target"); std::os::unix::fs::symlink(&trusted, &switch).expect("trusted PATH hop"); std::os::unix::fs::symlink(switch.join("git"), entry.join("git")) .expect("external candidate through PATH hop"); let locations = locations_for_root(&repo); let path = std::env::join_paths([&entry]).expect("PATH"); let runner = GitRunner::from_search_path(locations, &path).expect("initial trusted target"); std::fs::remove_file(&switch).expect("remove trusted PATH hop"); std::os::unix::fs::symlink(&attacker, &switch).expect("retarget PATH hop"); let output = runner.output(runner.command()).expect("run selected Git"); assert!(output.status.success(), "selected Git failed"); } #[cfg(unix)] #[test] fn resolver_rejects_same_identity_symlink_alias_to_repository() { let temp_base = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp dir"); let fixture = tempfile::tempdir_in(temp_base).expect("fixture"); let repo = fixture.path().join("repo"); let repo_bin = repo.join("bin"); let alias = fixture.path().join("repo-alias"); let alias_bin = alias.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&repo).expect("create repository"); write_git_candidate(&repo_bin); write_git_candidate(&trusted_bin); std::os::unix::fs::symlink(&repo, &alias).expect("symlink repository alias"); let locations = locations_for_root(&repo); assert_eq!( selected_git(&locations, &[&alias_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); } #[cfg(target_os = "macos")] #[test] fn resolver_rejects_apfs_data_firmlink_alias_to_repository() { let data_volume_temp = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp directory"); let fixture = tempfile::tempdir_in(&data_volume_temp).expect("fixture under Data volume"); let repo = fixture.path().join("repo"); let repo_bin = repo.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&repo).expect("create repository"); write_git_candidate(&repo_bin); write_git_candidate(&trusted_bin); let data_alias = PathBuf::from("/System/Volumes/Data") .join(repo.strip_prefix("/").expect("absolute repository path")); let alias_bin = data_alias.join("bin"); if std::fs::metadata(&data_alias).is_err() { eprintln!("APFS Data firmlink alias is unavailable; skipping native alias assertion"); return; } assert!( same_file::is_same_file(&data_alias, &repo).expect("compare APFS Data alias identity"), "APFS Data spelling does not identify the fixture repository" ); let locations = locations_for_root(&repo); assert_eq!( selected_git(&locations, &[&alias_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); } #[cfg(target_os = "macos")] #[test] fn metadata_routes_distinguish_apfs_worktree_hop_from_direct_metadata_alias() { use std::os::unix::fs::symlink; let data_volume_temp = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp directory"); let fixture = tempfile::tempdir_in(&data_volume_temp).expect("fixture under Data volume"); let outer = fixture.path().join("outer"); let root = outer.join("nested"); let admin = outer.join(".git/modules/nested"); std::fs::create_dir_all(&root).expect("create nested repository"); run_git(&outer, &["init", "-q"]); run_git(&root, &["init", "-q"]); std::fs::create_dir_all(admin.parent().expect("module metadata parent")) .expect("create module metadata parent"); std::fs::rename(root.join(".git"), &admin).expect("move metadata into outer .git"); let data_alias = PathBuf::from("/System/Volumes/Data") .join(outer.strip_prefix("/").expect("absolute repository path")); if std::fs::metadata(&data_alias).is_err() { eprintln!("APFS Data firmlink alias is unavailable; skipping native route assertion"); return; } assert!(same_file::is_same_file(&data_alias, &outer).expect("compare APFS root alias")); // A direct alternate spelling of proven outer metadata remains compatible. std::fs::write( root.join(".git"), format!( "gitdir: {}\n", data_alias.join(".git/modules/nested").display() ), ) .expect("write direct metadata alias marker"); let runner = GitRunner::for_cwd(&root).expect("direct APFS metadata alias"); let mut command = runner .command_for_cwd(&root) .expect("direct APFS alias command"); command.args(["rev-parse", "--absolute-git-dir"]); assert!( runner .output(command) .expect("direct APFS alias rev-parse") .status .success() ); // A different symlink entry reached through the same root identity is a // mutable worktree hop even when it exits to the same external metadata. symlink(&admin, outer.join("switch")).expect("worktree route switch"); std::fs::write( root.join(".git"), format!("gitdir: {}\n", data_alias.join("switch").display()), ) .expect("write APFS worktree-hop marker"); run_git(&root, &["rev-parse", "--absolute-git-dir"]); assert_unsafe_metadata_route(repository_authority_for_cwd(&root), &root.join(".git")); } #[cfg(windows)] #[test] fn resolver_rejects_same_identity_junction_alias_to_repository() { let fixture = tempfile::tempdir().expect("fixture"); let repo = fixture.path().join("repo"); let repo_bin = repo.join("bin"); let alias = fixture.path().join("repo-alias"); let alias_bin = alias.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&repo).expect("create repository"); write_git_candidate(&repo_bin); write_git_candidate(&trusted_bin); create_junction(&alias, &repo); let locations = locations_for_root(&repo); assert_eq!( selected_git(&locations, &[&alias_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); } #[cfg(unix)] #[test] fn gitdir_route_through_current_worktree_is_denied_before_path_selection() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repo"); let external = fixture.path().join("external"); let admin = external.join("admin"); std::fs::create_dir_all(&root).expect("create repository"); std::fs::create_dir_all(&external).expect("create external directory"); run_git(&root, &["init", "-q"]); std::fs::rename(root.join(".git"), &admin).expect("move metadata external"); symlink(&admin, root.join("switch")).expect("worktree metadata switch"); std::fs::write(root.join(".git"), "gitdir: switch\n").expect("write gitdir marker"); run_git(&root, &["rev-parse", "--absolute-git-dir"]); assert_unsafe_metadata_route(repository_authority_for_cwd(&root), &root.join(".git")); let error = GitRunner::for_cwd_io(&root).expect_err("unsafe metadata route"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied); assert!(error.to_string().contains("Git metadata route crosses")); assert!(!error.to_string().contains("PATH")); } #[cfg(unix)] #[test] fn gitdir_route_that_enters_and_exits_worktree_by_identity_is_denied() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repo"); let external = fixture.path().join("external"); let admin = external.join("admin"); let entry = external.join("entry"); std::fs::create_dir_all(&root).expect("create repository"); std::fs::create_dir_all(&external).expect("create external directory"); run_git(&root, &["init", "-q"]); std::fs::rename(root.join(".git"), &admin).expect("move metadata external"); symlink(&root, &entry).expect("external entry into worktree"); symlink(&admin, root.join("switch")).expect("worktree exit to metadata"); std::fs::write( root.join(".git"), format!("gitdir: {}\n", entry.join("switch").display()), ) .expect("write identity-hop gitdir marker"); run_git(&root, &["rev-parse", "--absolute-git-dir"]); assert_unsafe_metadata_route(repository_authority_for_cwd(&root), &root.join(".git")); } #[cfg(unix)] #[test] fn gitdir_symlink_target_cannot_hide_mutable_worktree_pivot_before_parent_traversal() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let primary = fixture.path().join("primary"); let current = fixture.path().join("current"); let attacker = fixture.path().join("attacker"); let external = fixture.path().join("external"); std::fs::create_dir_all(primary.join("pivot")).expect("create worktree pivot"); std::fs::create_dir_all(¤t).expect("create current worktree"); std::fs::create_dir_all(&attacker).expect("create attacker repository"); std::fs::create_dir_all(&external).expect("create external directory"); run_git(&primary, &["init", "-q"]); run_git(&attacker, &["init", "-q"]); symlink("../primary/pivot/../.git", external.join("entry")) .expect("external entry with hidden worktree pivot"); std::fs::write( current.join(".git"), format!("gitdir: {}\n", external.join("entry").display()), ) .expect("write current gitdir marker"); assert_eq!( std::fs::canonicalize(run_git_stdout( ¤t, &["rev-parse", "--absolute-git-dir"] )) .expect("canonical initial Git dir"), std::fs::canonicalize(primary.join(".git")).expect("canonical primary Git dir") ); assert_unsafe_metadata_route( repository_authority_for_cwd(¤t), ¤t.join(".git"), ); std::fs::remove_dir(primary.join("pivot")).expect("remove ordinary pivot"); std::fs::create_dir_all(attacker.join("nested")).expect("create attacker nested directory"); symlink(attacker.join("nested"), primary.join("pivot")).expect("retarget worktree pivot"); assert_eq!( std::fs::canonicalize(run_git_stdout( ¤t, &["rev-parse", "--absolute-git-dir"] )) .expect("canonical retargeted Git dir"), std::fs::canonicalize(attacker.join(".git")).expect("canonical attacker Git dir") ); assert_unsafe_metadata_route( repository_authority_for_cwd(¤t), ¤t.join(".git"), ); } #[cfg(unix)] #[test] fn standard_directory_cwd_cannot_hide_mutable_worktree_pivot() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let outer = fixture.path().join("outer"); let original = fixture.path().join("external-standard-repo"); let attacker = fixture.path().join("attacker"); let attacker_repo = attacker.join("external-standard-repo"); std::fs::create_dir_all(outer.join("pivot")).expect("create worktree pivot"); std::fs::create_dir_all(&original).expect("create original repository"); std::fs::create_dir_all(&attacker_repo).expect("create attacker repository"); run_git(&outer, &["init", "-q"]); run_git(&original, &["init", "-q"]); run_git(&attacker_repo, &["init", "-q"]); let raw = outer .join("pivot") .join("..") .join("..") .join("external-standard-repo"); assert_eq!( std::fs::canonicalize(run_git_stdout(&raw, &["rev-parse", "--show-toplevel"])) .expect("canonical initial Git root"), std::fs::canonicalize(&original).expect("canonical original repository") ); let exact_root_route = outer.join("..").join("external-standard-repo"); repository_authority_for_cwd(&exact_root_route) .expect("exact worktree root traversal remains valid"); assert_unsafe_metadata_route(repository_authority_for_cwd(&raw), &raw.join(".git")); std::fs::remove_dir(outer.join("pivot")).expect("remove ordinary pivot"); let attacker_target = attacker.join("deep/nested"); std::fs::create_dir_all(&attacker_target).expect("create attacker pivot target"); symlink(&attacker_target, outer.join("pivot")).expect("retarget worktree pivot"); assert_eq!( std::fs::canonicalize(run_git_stdout(&raw, &["rev-parse", "--show-toplevel"])) .expect("canonical retargeted Git root"), std::fs::canonicalize(&attacker_repo).expect("canonical attacker repository") ); assert_unsafe_metadata_route(repository_authority_for_cwd(&raw), &raw.join(".git")); } #[cfg(unix)] #[test] fn command_cwd_is_bound_before_worktree_symlink_retarget() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let outer = fixture.path().join("outer"); let original = fixture.path().join("original"); let attacker = fixture.path().join("attacker"); let alias = outer.join("nested"); for repository in [&outer, &original, &attacker] { std::fs::create_dir_all(repository).expect("create repository"); run_git(repository, &["init", "-q"]); } symlink(&original, &alias).expect("create worktree-controlled cwd alias"); let runner = GitRunner::for_cwd(&alias).expect("runner for cwd alias"); let mut pinned = runner .command_for_cwd(&alias) .expect("command pinned before retarget"); pinned.args(["rev-parse", "--show-toplevel"]); std::fs::remove_file(&alias).expect("remove original cwd alias"); symlink(&attacker, &alias).expect("retarget cwd alias"); let error = match runner.command_for_cwd(&alias) { Ok(_) => panic!("retargeted cwd alias was accepted"), Err(error) => error, }; assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); assert!( error .to_string() .contains("no longer resolves within the selected worktree"), "{error}" ); let output = runner.output(pinned).expect("run pinned cwd command"); assert!(output.status.success()); assert_eq!( std::fs::canonicalize(String::from_utf8_lossy(&output.stdout).trim()) .expect("canonical pinned Git root"), std::fs::canonicalize(&original).expect("canonical original repository") ); } #[cfg(windows)] #[test] fn command_cwd_is_bound_before_worktree_junction_retarget() { let fixture = tempfile::tempdir().expect("fixture"); let outer = fixture.path().join("outer"); let original = fixture.path().join("original"); let attacker = fixture.path().join("attacker"); let alias = outer.join("nested"); for repository in [&outer, &original, &attacker] { std::fs::create_dir_all(repository).expect("create repository"); run_git(repository, &["init", "-q"]); } create_junction(&alias, &original); let runner = GitRunner::for_cwd(&alias).expect("runner for cwd junction"); let mut pinned = runner .command_for_cwd(&alias) .expect("command pinned before retarget"); pinned.args(["rev-parse", "--show-toplevel"]); std::fs::remove_dir(&alias).expect("remove original cwd junction"); create_junction(&alias, &attacker); let error = match runner.command_for_cwd(&alias) { Ok(_) => panic!("retargeted cwd junction was accepted"), Err(error) => error, }; assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); let output = runner.output(pinned).expect("run pinned cwd command"); assert!(output.status.success()); assert_eq!( std::fs::canonicalize(String::from_utf8_lossy(&output.stdout).trim()) .expect("canonical pinned Git root"), std::fs::canonicalize(&original).expect("canonical original repository") ); } #[cfg(windows)] #[test] fn command_for_cwd_executes_from_a_canonical_windows_path() { let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repo"); std::fs::create_dir_all(&root).expect("create repository"); run_git(&root, &["init", "-q"]); let canonical_root = std::fs::canonicalize(&root).expect("canonical repository"); let runner = GitRunner::for_cwd(&root).expect("runner for canonical cwd"); let mut command = runner .command_for_cwd(&canonical_root) .expect("command for canonical cwd"); command.args(["rev-parse", "--show-toplevel"]); let output = runner.output(command).expect("run Git from canonical cwd"); assert!( output.status.success(), "Git rejected canonical cwd {}: {}", canonical_root.display(), String::from_utf8_lossy(&output.stderr) ); assert_eq!( std::fs::canonicalize(String::from_utf8_lossy(&output.stdout).trim()) .expect("canonical Git root"), canonical_root ); } #[test] fn gitdir_terminal_worktree_root_is_never_promoted_to_metadata() { let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repo"); run_git(fixture.path(), &["init", "--bare", path_text(&root)]); std::fs::write(root.join(".git"), "gitdir: .\n").expect("write self-root marker"); assert_eq!( std::fs::canonicalize(run_git_stdout(&root, &["rev-parse", "--absolute-git-dir"])) .expect("canonical native self-root Git dir"), std::fs::canonicalize(&root).expect("canonical worktree root") ); assert_unsafe_metadata_route(repository_authority_for_cwd(&root), &root.join(".git")); } #[cfg(unix)] #[test] fn commondir_routes_through_worktree_are_denied_before_path_selection() { use std::os::unix::fs::symlink; for identity_entry in [false, true] { let fixture = tempfile::tempdir().expect("fixture"); let primary = fixture.path().join("primary"); let linked = fixture.path().join("linked"); let entry = fixture.path().join("entry"); std::fs::create_dir_all(&primary).expect("create primary"); run_git(&primary, &["init", "-q"]); run_git( &primary, &["worktree", "add", "--orphan", path_text(&linked)], ); let admin = PathBuf::from({ let mut command = Command::new("git"); isolate_git_command_environment(&mut command); let output = command .args(["rev-parse", "--absolute-git-dir"]) .current_dir(&linked) .output() .expect("resolve linked admin"); assert!(output.status.success()); String::from_utf8(output.stdout) .expect("UTF-8 linked admin") .trim() .to_string() }); symlink(primary.join(".git"), linked.join("switch")).expect("worktree common-dir switch"); let route = if identity_entry { symlink(&linked, &entry).expect("external entry into linked worktree"); entry.join("switch") } else { linked.join("switch") }; std::fs::write(admin.join("commondir"), format!("{}\n", route.display())) .expect("rewrite commondir route"); run_git(&linked, &["rev-parse", "--git-common-dir"]); assert_unsafe_metadata_route(repository_authority_for_cwd(&linked), &linked.join(".git")); } } #[cfg(unix)] #[test] fn active_gitdir_route_is_revalidated_before_every_child() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repo"); let admin = fixture.path().join("external-admin"); std::fs::create_dir_all(&root).expect("create repository"); run_git( fixture.path(), &[ "init", "--separate-git-dir", path_text(&admin), path_text(&root), ], ); let runner = GitRunner::for_cwd(&root).expect("runner for direct external gitdir"); let mut first = runner.command_for_cwd(&root).expect("first Git command"); first.args(["rev-parse", "--absolute-git-dir"]); assert!( runner .output(first) .expect("first Git child") .status .success() ); symlink(&admin, root.join("switch")).expect("worktree gitdir switch"); std::fs::write(root.join(".git"), "gitdir: switch\n").expect("retarget gitdir marker"); let sentinel = fixture.path().join("git-child-ran"); let mut second = runner.command(); second .args(["config", "--file"]) .arg(&sentinel) .args(["probe.value", "ran"]); let error = runner .output(second) .expect_err("retargeted gitdir must block second child"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); assert!( !sentinel.exists(), "Git child executed after gitdir retarget" ); } #[cfg(unix)] #[test] fn active_commondir_route_is_revalidated_before_every_child() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let primary = fixture.path().join("primary"); let linked = fixture.path().join("linked"); std::fs::create_dir_all(&primary).expect("create primary"); run_git(&primary, &["init", "-q"]); run_git( &primary, &["worktree", "add", "--orphan", path_text(&linked)], ); let runner = GitRunner::for_cwd(&linked).expect("runner for linked worktree"); let admin = runner .authority .active_git_dir() .expect("active linked metadata") .to_path_buf(); let mut first = runner.command_for_cwd(&linked).expect("first Git command"); first.args(["rev-parse", "--git-common-dir"]); assert!( runner .output(first) .expect("first Git child") .status .success() ); symlink(primary.join(".git"), linked.join("switch")).expect("worktree common-dir switch"); std::fs::write( admin.join("commondir"), format!("{}\n", linked.join("switch").display()), ) .expect("retarget commondir marker"); let sentinel = fixture.path().join("git-child-ran"); let mut second = runner.command(); second .args(["config", "--file"]) .arg(&sentinel) .args(["probe.value", "ran"]); let error = runner .output(second) .expect_err("retargeted commondir must block second child"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); assert!( !sentinel.exists(), "Git child executed after commondir retarget" ); } #[cfg(unix)] #[test] fn direct_external_separate_git_dir_absolute_and_relative_routes_execute_git() { for relative in [false, true] { let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repo"); let admin = fixture.path().join("external-admin"); std::fs::create_dir_all(&root).expect("create repository"); run_git( fixture.path(), &[ "init", "--separate-git-dir", path_text(&admin), path_text(&root), ], ); if relative { std::fs::write(root.join(".git"), "gitdir: ../external-admin\n") .expect("write relative gitdir marker"); } run_git(&root, &["rev-parse", "--absolute-git-dir"]); let runner = GitRunner::for_cwd(&root).expect("separate-git-dir runner"); let mut command = runner .command_for_cwd(&root) .expect("separate-git-dir command"); command.args(["rev-parse", "--absolute-git-dir"]); crate::repository_authority::reset_bounded_marker_read_count(); assert!( runner .output(command) .expect("separate-git-dir rev-parse") .status .success(), "relative={relative}" ); assert_eq!( crate::repository_authority::bounded_marker_read_count(), 1, "separate Git dir should reread only the active .git marker" ); } } #[cfg(unix)] #[test] fn trusted_external_gitdir_symlink_chain_executes_git() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let root = fixture.path().join("repo"); let external = fixture.path().join("external"); let admin = external.join("admin"); let alias_two = external.join("alias-two"); let alias_one = external.join("alias-one"); std::fs::create_dir_all(&root).expect("create repository"); std::fs::create_dir_all(&external).expect("create external directory"); run_git(&root, &["init", "-q"]); std::fs::rename(root.join(".git"), &admin).expect("move metadata external"); symlink(&admin, &alias_two).expect("second trusted alias"); symlink(&alias_two, &alias_one).expect("first trusted alias"); std::fs::write( root.join(".git"), format!("gitdir: {}\n", alias_one.display()), ) .expect("write trusted-chain marker"); run_git(&root, &["rev-parse", "--absolute-git-dir"]); let runner = GitRunner::for_cwd(&root).expect("trusted-chain runner"); let mut command = runner .command_for_cwd(&root) .expect("trusted-chain command"); command.args(["rev-parse", "--absolute-git-dir"]); assert!( runner .output(command) .expect("trusted-chain rev-parse") .status .success() ); } #[test] fn displaced_linked_admin_file_and_directory_routes_execute_git() { for directory_marker in [false, true] { for relative_common in [false, true] { let fixture = tempfile::tempdir().expect("fixture"); let primary = fixture.path().join("primary"); let linked = fixture.path().join("linked"); let displaced = fixture.path().join("external/admin"); std::fs::create_dir_all(&primary).expect("create primary"); run_git(&primary, &["init", "-q"]); run_git( &primary, &["worktree", "add", "--orphan", path_text(&linked)], ); let original_admin = PathBuf::from(run_git_stdout( &linked, &["rev-parse", "--absolute-git-dir"], )); let admin = if directory_marker { std::fs::remove_file(linked.join(".git")).expect("remove linked marker"); std::fs::rename(&original_admin, linked.join(".git")) .expect("move admin to directory marker"); linked.join(".git") } else { std::fs::create_dir_all(displaced.parent().expect("displaced parent")) .expect("create displaced parent"); std::fs::rename(&original_admin, &displaced).expect("displace linked admin"); std::fs::write( linked.join(".git"), format!("gitdir: {}\n", displaced.display()), ) .expect("redirect linked marker"); displaced.clone() }; let common = if relative_common { "../../primary/.git".to_string() } else { primary.join(".git").display().to_string() }; std::fs::write(admin.join("commondir"), format!("{common}\n")) .expect("rewrite displaced commondir"); run_git(&linked, &["rev-parse", "--git-common-dir"]); let runner = GitRunner::for_cwd(&linked).expect("displaced-admin runner"); let mut command = runner .command_for_cwd(&linked) .expect("displaced-admin command"); command.args(["rev-parse", "--git-common-dir"]); crate::repository_authority::reset_bounded_marker_read_count(); assert!( runner .output(command) .expect("displaced-admin rev-parse") .status .success(), "directory_marker={directory_marker} relative_common={relative_common}" ); assert_eq!( crate::repository_authority::bounded_marker_read_count(), if directory_marker { 1 } else { 2 }, "displaced active marker read count" ); } } } #[test] fn linked_worktree_rejects_git_from_main_and_linked_worktrees() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let linked = fixture.path().join("linked"); let main_bin = main.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); write_git_candidate(&main_bin); write_runnable_git_candidate(&trusted_bin); let locations = repository_authority_for_cwd(&linked).expect("untrusted locations"); assert!(path_is_untrusted( &main_bin.join(git_executable_name()), &locations )); let runner = GitRunner::from_search_path( locations, &std::env::join_paths([&main_bin, &trusted_bin]).expect("PATH"), ) .expect("linked-worktree runner"); let mut command = runner .command_for_cwd(&linked) .expect("linked-worktree command"); command.args(["rev-parse", "--show-toplevel"]); crate::repository_authority::reset_bounded_marker_read_count(); assert!( runner .output(command) .expect("linked-worktree rev-parse") .status .success() ); assert_eq!( crate::repository_authority::bounded_marker_read_count(), 2, "linked worktree should reread active .git and commondir markers" ); } #[test] fn registered_sibling_remains_untrusted_without_its_worktree_marker_or_root() { let fixture = tempdir_for_native_git(); let main = fixture.path().join("main"); let sibling = fixture.path().join("sibling"); let sibling_bin = sibling.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&sibling)]); write_git_candidate(&sibling_bin); write_git_candidate(&trusted_bin); for state in ["intact", "marker removed", "root recreated"] { if state == "marker removed" { std::fs::remove_file(sibling.join(".git")).expect("remove sibling .git marker"); } else if state == "root recreated" { std::fs::remove_dir_all(&sibling).expect("remove sibling root"); write_git_candidate(&sibling_bin); } let locations = repository_authority_for_cwd(&main).expect("untrusted locations"); assert!( path_is_untrusted(&sibling_bin.join(git_executable_name()), &locations), "registered sibling Git became trusted in state {state}" ); assert_eq!( selected_git(&locations, &[&sibling_bin, &trusted_bin]), trusted_bin.join(git_executable_name()), "registered sibling Git selected in state {state}" ); } } #[test] fn git_generated_relative_registry_route_is_accepted() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let linked = fixture.path().join("linked"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); let admin = PathBuf::from(run_git_stdout( &linked, &["rev-parse", "--absolute-git-dir"], )); let relative_marker = PathBuf::from("..") .join("..") .join("..") .join("..") .join("linked") .join(".git"); std::fs::write( admin.join("gitdir"), format!("{}\n", relative_marker.display()), ) .expect("write Git-style relative registry marker"); let authority = repository_authority_for_cwd(&main).expect("relative registry authority"); assert!( authority .contains_root(&std::fs::canonicalize(&linked).expect("canonical linked worktree")) ); GitRunner::for_cwd(&linked).expect("relative linked-worktree runner"); } #[test] fn registered_worktree_route_rejects_canceled_worktree_descendant() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let linked = fixture.path().join("linked"); std::fs::create_dir_all(main.join("pivot")).expect("create worktree pivot"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); let admin = PathBuf::from(run_git_stdout( &linked, &["rev-parse", "--absolute-git-dir"], )); let registry_marker = admin.join("gitdir"); let raw_marker = main.join("pivot").join("..").join("..").join("linked/.git"); std::fs::write(®istry_marker, format!("{}\n", raw_marker.display())) .expect("write registry route through worktree pivot"); assert_unsafe_registry_route(repository_authority_for_cwd(&main), ®istry_marker); let error = GitRunner::for_cwd_io(&main).expect_err("unsafe registry route"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied); } #[test] fn registered_route_rejects_root_that_is_also_outer_worktree_descendant() { let fixture = tempfile::tempdir().expect("fixture"); let route = overlapping_registered_route(fixture.path()); assert_eq!( std::fs::canonicalize(&route.raw_marker).expect("canonical linked marker"), std::fs::canonicalize(route.linked.join(".git")).expect("canonical expected marker") ); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); } #[test] fn registered_route_allows_parent_cancellation_inside_protected_metadata() { let fixture = tempfile::tempdir().expect("fixture"); let route = metadata_alias_registered_route(fixture.path(), "main", "main"); let protected_marker = route.main.join(".git").join("..").join("linked/.git"); std::fs::write( &route.registry_marker, format!("{}\n", protected_marker.display()), ) .expect("write protected-metadata registry route"); assert_eq!( std::fs::canonicalize(&protected_marker).expect("canonical protected route"), std::fs::canonicalize(route.linked.join(".git")).expect("canonical linked marker") ); repository_authority_for_cwd(&route.main).expect("protected-metadata registry authority"); GitRunner::for_cwd(&route.linked).expect("linked runner through protected metadata route"); } #[cfg(unix)] #[test] fn metadata_alias_registered_route_is_rejected_before_and_after_symlink_retarget() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let route = metadata_alias_registered_route(fixture.path(), "main", "main"); symlink(route.main.join(".git"), &route.pivot).expect("alias pivot to protected metadata"); assert_eq!( std::fs::canonicalize(&route.raw_marker).expect("canonical metadata-alias route"), std::fs::canonicalize(route.linked.join(".git")).expect("canonical linked marker") ); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); std::fs::remove_file(&route.pivot).expect("remove metadata alias"); let attacker_target = fixture.path().join("attacker/deep"); let attacker_linked = fixture.path().join("attacker/linked"); std::fs::create_dir_all(&attacker_target).expect("create attacker pivot target"); std::fs::create_dir_all(&attacker_linked).expect("create attacker linked root"); std::fs::write( attacker_linked.join(".git"), format!("gitdir: {}\n", route.linked_admin.display()), ) .expect("write attacker backlink"); symlink(&attacker_target, &route.pivot).expect("retarget metadata alias"); assert_eq!( std::fs::canonicalize(&route.raw_marker).expect("canonical retargeted route"), std::fs::canonicalize(attacker_linked.join(".git")).expect("canonical attacker marker") ); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); } #[cfg(windows)] #[test] fn registered_route_rejects_unicode_case_junction_retarget() { let fixture = tempfile::tempdir().expect("fixture"); let route = metadata_alias_registered_route(fixture.path(), "Répo", "RÉPO"); let raw_marker = fixture.path().join("RÉPO").join("pivot").join(".git"); std::fs::write( &route.registry_marker, format!("{}\n", raw_marker.display()), ) .expect("write direct junction registry route"); create_junction(&route.pivot, &route.linked); assert_eq!( std::fs::canonicalize(&raw_marker).expect("canonical Unicode junction route"), std::fs::canonicalize(route.linked.join(".git")).expect("canonical linked marker") ); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); std::fs::remove_dir(&route.pivot).expect("remove metadata junction"); let attacker_linked = fixture.path().join("attacker/linked"); std::fs::create_dir_all(&attacker_linked).expect("create attacker linked root"); std::fs::write( attacker_linked.join(".git"), format!("gitdir: {}\n", route.linked_admin.display()), ) .expect("write attacker backlink"); create_junction(&route.pivot, &attacker_linked); assert_eq!( std::fs::canonicalize(&raw_marker).expect("canonical retargeted route"), std::fs::canonicalize(attacker_linked.join(".git")).expect("canonical attacker marker") ); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); } #[cfg(unix)] #[test] fn overlapping_registered_route_remains_rejected_after_symlink_retarget() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let route = overlapping_registered_route(fixture.path()); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); std::fs::remove_dir_all(&route.nested).expect("remove nested worktree"); let attacker_parent = fixture.path().join("attacker/deep"); let attacker_nested = attacker_parent.join("nested"); let attacker_linked = attacker_parent.join("linked"); std::fs::create_dir_all(&attacker_nested).expect("create attacker nested root"); std::fs::create_dir_all(&attacker_linked).expect("create attacker linked root"); std::fs::write( attacker_nested.join(".git"), format!("gitdir: {}\n", route.nested_admin.display()), ) .expect("write attacker nested backlink"); std::fs::write( attacker_linked.join(".git"), format!("gitdir: {}\n", route.linked_admin.display()), ) .expect("write attacker linked backlink"); symlink(&attacker_nested, &route.nested).expect("retarget nested worktree root"); assert_eq!( std::fs::canonicalize(&route.raw_marker).expect("canonical retargeted marker"), std::fs::canonicalize(attacker_linked.join(".git")).expect("canonical attacker marker") ); let nested_registry_marker = route.nested_admin.join("gitdir"); assert_unsafe_registry_route_at_one_of( repository_authority_for_cwd(&route.main), &[&route.registry_marker, &nested_registry_marker], ); } #[cfg(windows)] #[test] fn overlapping_registered_route_remains_rejected_after_junction_retarget() { let fixture = tempfile::tempdir().expect("fixture"); let route = overlapping_registered_route(fixture.path()); let pivot = route.nested.join("pivot"); let raw_marker = pivot.join("linked/.git"); std::fs::write( &route.registry_marker, format!("{}\n", raw_marker.display()), ) .expect("write direct overlapping registry route"); create_junction(&pivot, &route.main); assert_eq!( std::fs::canonicalize(&raw_marker).expect("canonical overlapping junction route"), std::fs::canonicalize(route.linked.join(".git")).expect("canonical linked marker") ); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); std::fs::remove_dir(&pivot).expect("remove overlapping junction"); let attacker_parent = fixture.path().join("attacker/deep"); let attacker_linked = attacker_parent.join("linked"); std::fs::create_dir_all(&attacker_linked).expect("create attacker linked root"); std::fs::write( attacker_linked.join(".git"), format!("gitdir: {}\n", route.linked_admin.display()), ) .expect("write attacker linked backlink"); create_junction(&pivot, &attacker_parent); assert_eq!( std::fs::canonicalize(&raw_marker).expect("canonical retargeted marker"), std::fs::canonicalize(attacker_linked.join(".git")).expect("canonical attacker marker") ); assert_unsafe_registry_route( repository_authority_for_cwd(&route.main), &route.registry_marker, ); } #[cfg(unix)] #[test] fn registered_worktree_route_remains_rejected_after_pivot_retarget() { use std::os::unix::fs::symlink; let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let linked = fixture.path().join("linked"); let attacker = fixture.path().join("attacker"); std::fs::create_dir_all(main.join("pivot")).expect("create worktree pivot"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); let admin = PathBuf::from(run_git_stdout( &linked, &["rev-parse", "--absolute-git-dir"], )); let registry_marker = admin.join("gitdir"); let raw_marker = main.join("pivot").join("..").join("..").join("linked/.git"); std::fs::write(®istry_marker, format!("{}\n", raw_marker.display())) .expect("write registry route through worktree pivot"); assert_unsafe_registry_route(repository_authority_for_cwd(&main), ®istry_marker); std::fs::remove_dir(main.join("pivot")).expect("remove ordinary pivot"); let attacker_target = attacker.join("deep/nested"); let attacker_worktree = attacker.join("linked"); std::fs::create_dir_all(&attacker_target).expect("create attacker pivot target"); std::fs::create_dir_all(&attacker_worktree).expect("create attacker worktree"); std::fs::write( attacker_worktree.join(".git"), format!("gitdir: {}\n", admin.display()), ) .expect("write attacker backlink"); symlink(&attacker_target, main.join("pivot")).expect("retarget worktree pivot"); assert_unsafe_registry_route(repository_authority_for_cwd(&main), ®istry_marker); assert!(matches!( GitRunner::for_cwd(&main), Err(GitReadError::UnsafeRepositoryMetadata { .. }) )); } #[test] fn registered_worktree_backlink_mismatch_is_unsafe() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let linked = fixture.path().join("linked"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); let marker = linked.join(".git"); std::fs::write( &marker, format!("gitdir: {}\n", main.join(".git").display()), ) .expect("rewrite linked-worktree backlink"); match repository_authority_for_cwd(&main) { Err(GitReadError::UnsafeRepositoryMetadata { path, reason }) => { assert_same_affected_path(&path, &marker); assert_eq!(reason, "Git worktree registry backlink mismatch"); } other => panic!("expected unsafe registry backlink, got {other:?}"), } let error = GitRunner::for_cwd_io(&main).expect_err("mismatched worktree backlink"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied); assert!(error.to_string().contains("backlink mismatch"), "{error}"); assert!(!error.to_string().contains("PATH"), "{error}"); } #[test] fn unsupported_worktree_registry_entry_is_invalid_with_entry_path() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); let registry = main.join(".git/worktrees"); std::fs::create_dir_all(®istry).expect("create worktree registry"); let entry = registry.join("unsupported-entry"); std::fs::write(&entry, "not a registry directory\n").expect("write invalid registry entry"); match repository_authority_for_cwd(&main) { Err(GitReadError::InvalidRepositoryMetadata { path, reason }) => { assert_same_affected_path(&path, &entry); assert_eq!(reason, "unsupported Git worktree registry entry"); } other => panic!("expected invalid registry entry, got {other:?}"), } let error = GitRunner::for_cwd_io(&main).expect_err("unsupported registry entry"); assert_eq!(error.kind(), io::ErrorKind::InvalidData); assert!( error .to_string() .contains("unsupported Git worktree registry entry"), "{error}" ); assert!(!error.to_string().contains("PATH"), "{error}"); } #[test] fn per_child_active_route_revalidation_is_independent_of_registered_sibling_count() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let active = fixture.path().join("active"); std::fs::create_dir_all(&main).expect("create primary"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&active)]); for index in 0..12 { let sibling = fixture.path().join(format!("sibling-{index}")); run_git(&main, &["worktree", "add", "--orphan", path_text(&sibling)]); } let runner = GitRunner::for_cwd(&active).expect("runner with many registered siblings"); crate::repository_authority::reset_bounded_marker_read_count(); let mut command = runner .command_for_cwd(&active) .expect("active worktree command"); command.args(["rev-parse", "--git-common-dir"]); assert!( runner .output(command) .expect("active worktree rev-parse") .status .success() ); assert_eq!( crate::repository_authority::bounded_marker_read_count(), 2, "per-child validation must read only active .git and commondir markers" ); } #[test] fn nested_repository_rejects_git_from_enclosing_repository() { let fixture = tempfile::tempdir().expect("fixture"); let outer = fixture.path().join("outer"); let nested = outer.join("nested"); let outer_bin = outer.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&nested).expect("create nested repository"); run_git(&outer, &["init", "-q"]); run_git(&nested, &["init", "-q"]); write_git_candidate(&outer_bin); write_git_candidate(&trusted_bin); let locations = repository_authority_for_cwd(&nested).expect("untrusted locations"); assert!( path_is_untrusted(&outer_bin.join(git_executable_name()), &locations), "Git from an enclosing repository must remain repository-controlled" ); assert_eq!( selected_git(&locations, &[&outer_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); } #[cfg(unix)] #[test] fn symlinked_nested_repository_rejects_git_from_lexical_enclosing_repository() { let fixture = tempfile::tempdir().expect("fixture"); let outer = fixture.path().join("outer"); let physical_nested = fixture.path().join("physical-nested"); let lexical_nested = outer.join("nested"); let outer_bin = outer.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&outer).expect("create outer repository"); std::fs::create_dir_all(&physical_nested).expect("create physical nested repository"); run_git(&outer, &["init", "-q"]); run_git(&physical_nested, &["init", "-q"]); std::os::unix::fs::symlink(&physical_nested, &lexical_nested) .expect("symlink nested repository"); write_git_candidate(&outer_bin); write_git_candidate(&trusted_bin); let locations = repository_authority_for_cwd(&lexical_nested).expect("untrusted locations"); assert!( path_is_untrusted(&outer_bin.join(git_executable_name()), &locations), "Git from the lexical enclosing repository must remain repository-controlled" ); assert_eq!( selected_git(&locations, &[&outer_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); } #[test] fn nested_repository_rejects_git_from_enclosing_repository_main_worktree() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let linked = fixture.path().join("linked"); let nested = linked.join("nested"); let main_bin = main.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&main).expect("create main worktree"); run_git(&main, &["init", "-q"]); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); std::fs::create_dir_all(&nested).expect("create nested repository"); run_git(&nested, &["init", "-q"]); write_git_candidate(&main_bin); write_git_candidate(&trusted_bin); let locations = repository_authority_for_cwd(&nested).expect("untrusted locations"); assert!( path_is_untrusted(&main_bin.join(git_executable_name()), &locations), "all worktrees of an enclosing repository must remain repository-controlled" ); assert_eq!( selected_git(&locations, &[&main_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); } #[test] fn submodule_rejects_git_from_enclosing_superproject() { let fixture = tempfile::tempdir().expect("fixture"); let source = fixture.path().join("source"); let outer = fixture.path().join("outer"); let submodule = outer.join("nested"); let outer_bin = outer.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&source).expect("create source repository"); std::fs::create_dir_all(&outer).expect("create superproject"); run_git(&source, &["init", "-q"]); std::fs::write(source.join("source.txt"), "source\n").expect("write source file"); run_git(&source, &["add", "source.txt"]); commit_all(&source, "source"); run_git(&outer, &["init", "-q"]); std::fs::write(outer.join("outer.txt"), "outer\n").expect("write outer file"); run_git(&outer, &["add", "outer.txt"]); commit_all(&outer, "outer"); run_git( &outer, &[ "-c", "protocol.file.allow=always", "submodule", "add", "-q", path_text(&source), "nested", ], ); write_git_candidate(&outer_bin); write_runnable_git_candidate(&trusted_bin); let locations = repository_authority_for_cwd(&submodule).expect("untrusted locations"); assert!( path_is_untrusted(&outer_bin.join(git_executable_name()), &locations), "Git from a superproject must remain repository-controlled" ); assert_eq!( selected_git(&locations, &[&outer_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); let runner = GitRunner::from_search_path( locations, &std::env::join_paths([&outer_bin, &trusted_bin]).expect("PATH"), ) .expect("submodule runner"); let mut command = runner .command_for_cwd(&submodule) .expect("submodule command"); command.args(["rev-parse", "--show-toplevel"]); crate::repository_authority::reset_bounded_marker_read_count(); assert!( runner .output(command) .expect("submodule rev-parse") .status .success() ); assert_eq!( crate::repository_authority::bounded_marker_read_count(), 1, "submodule should reread only its active .git marker" ); } #[test] fn bare_backed_linked_worktree_allows_external_git_in_sibling_directory() { let fixture = tempfile::tempdir().expect("fixture"); let bare = fixture.path().join("repository.git"); let linked = fixture.path().join("linked"); let trusted_bin = fixture.path().join("trusted-bin"); run_git(fixture.path(), &["init", "--bare", path_text(&bare)]); run_git( fixture.path(), &[ "--git-dir", path_text(&bare), "worktree", "add", "--orphan", path_text(&linked), ], ); write_runnable_git_candidate(&trusted_bin); let locations = repository_authority_for_cwd(&linked).expect("untrusted locations"); assert_eq!( selected_git(&locations, &[&trusted_bin]), trusted_bin.join(git_executable_name()) ); let runner = GitRunner::from_search_path( locations, &std::env::join_paths([&trusted_bin]).expect("PATH"), ) .expect("bare-backed runner"); let mut command = runner .command_for_cwd(&linked) .expect("bare-backed command"); command.args(["rev-parse", "--git-common-dir"]); crate::repository_authority::reset_bounded_marker_read_count(); assert!( runner .output(command) .expect("bare-backed rev-parse") .status .success() ); assert_eq!( crate::repository_authority::bounded_marker_read_count(), 2, "bare-backed linked worktree should reread two active markers" ); } #[test] fn linked_common_dir_nested_in_another_repo_is_rejected_before_path_selection() { let fixture = tempfile::tempdir().expect("fixture"); let outer = fixture.path().join("outer"); let common = outer.join("nested-common.git"); let linked = fixture.path().join("linked"); run_git(fixture.path(), &["init", path_text(&outer)]); std::fs::write(outer.join("seed.txt"), "seed\n").expect("write outer seed"); run_git(&outer, &["add", "seed.txt"]); commit_all(&outer, "outer seed"); run_git(&outer, &["clone", "--bare", ".", path_text(&common)]); run_git( fixture.path(), &[ "--git-dir", path_text(&common), "worktree", "add", "-b", "nested-common-linked", path_text(&linked), "HEAD", ], ); assert_unsafe_metadata_route(repository_authority_for_cwd(&linked), &linked.join(".git")); } #[test] fn separate_dot_git_dir_rejects_main_candidate_and_allows_unrelated_repo_candidate() { let fixture = tempfile::tempdir().expect("fixture"); let main = fixture.path().join("main"); let common_dir = fixture.path().join("git-storage/.git"); let linked = fixture.path().join("linked"); let main_bin = main.join("bin"); let unrelated = fixture.path().join("unrelated"); let unrelated_bin = unrelated.join("bin"); let malformed = fixture.path().join("malformed"); let malformed_bin = malformed.join("bin"); std::fs::create_dir_all(&main).expect("create main worktree"); std::fs::create_dir_all(common_dir.parent().expect("common-dir parent")) .expect("create common-dir parent"); run_git( fixture.path(), &[ "init", "--separate-git-dir", path_text(&common_dir), path_text(&main), ], ); run_git(&main, &["worktree", "add", "--orphan", path_text(&linked)]); run_git(fixture.path(), &["init", path_text(&unrelated)]); write_git_candidate(&main_bin); write_git_candidate(&unrelated_bin); write_git_candidate(&malformed_bin); std::fs::write(malformed.join(".git"), "not a gitdir").expect("malformed marker"); let locations = repository_authority_for_cwd(&linked).expect("untrusted locations"); assert_eq!( selected_git(&locations, &[&main_bin, &malformed_bin, &unrelated_bin]), unrelated_bin.join(git_executable_name()) ); } #[cfg(unix)] #[test] fn unproven_separate_primary_is_denied_before_any_path_git_executes() { let temp_base = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp dir"); for authority_config in ["non-bare", "included false", "bare 08", "bare overflow"] { let fixture = tempfile::tempdir_in(&temp_base).expect("fixture"); let primary = fixture.path().join("primary"); let common = fixture.path().join("separate-common"); let linked = fixture.path().join("linked"); let primary_bin = primary.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); let executed = fixture.path().join("attacker-git-ran"); std::fs::create_dir_all(&primary).expect("create primary"); run_git( fixture.path(), &[ "init", "--separate-git-dir", path_text(&common), path_text(&primary), ], ); std::fs::write(primary.join("seed"), "seed\n").expect("write seed"); run_git(&primary, &["add", "seed"]); commit_all(&primary, "seed"); run_git( &primary, &[ "worktree", "add", "-b", "unproven-primary-linked", path_text(&linked), ], ); if authority_config == "included false" { let included = primary.join("bare-override.config"); std::fs::write(&included, "[core]\n\tbare = false\n") .expect("write included bare override"); let mut config = std::fs::read_to_string(common.join("config")).expect("read common config"); config.push_str(&format!( "\n[core]\n\tbare = true\n[include]\n\tpath = {}\n", included.display() )); std::fs::write(common.join("config"), config).expect("write ambiguous bare config"); } else if let Some(value) = match authority_config { "bare 08" => Some("08"), "bare overflow" => Some("2147483648"), _ => None, } { let mut config = std::fs::read_to_string(common.join("config")).expect("read common config"); config.push_str(&format!("\n[core]\n\tbare = {value}\n")); std::fs::write(common.join("config"), config).expect("write malformed bare config"); } write_git_candidate(&trusted_bin); std::fs::create_dir_all(&primary_bin).expect("create primary bin"); std::fs::copy("/usr/bin/touch", primary_bin.join("git")) .expect("copy attacker native executable"); std::fs::remove_file(primary.join(".git")).expect("remove primary reverse marker"); let locations = repository_authority_for_cwd(&linked).expect("untrusted locations"); let path = std::env::join_paths([&primary_bin, &trusted_bin]).expect("PATH"); let error = match GitRunner::from_search_path(locations, &path) { Ok(runner) => { let mut command = runner.command(); command.arg(&executed); let _ = runner.output(command); panic!("unproven separate primary selected a PATH Git"); } Err(error) => error, }; assert!(!executed.exists(), "attacker Git executed"); assert!(matches!( error, GitReadError::UnprovenPrimaryAuthority { .. } )); } } #[cfg(unix)] #[test] fn nested_repo_inside_linked_outer_denies_unproven_outer_primary_before_path_git() { let temp_base = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp dir"); let fixture = tempfile::tempdir_in(temp_base).expect("fixture"); let primary = fixture.path().join("primary"); let common = fixture.path().join("separate-common"); let outer = fixture.path().join("outer"); let nested = outer.join("nested"); let primary_bin = primary.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); let executed = fixture.path().join("attacker-git-ran"); std::fs::create_dir_all(&primary).expect("create primary"); run_git( fixture.path(), &[ "init", "--separate-git-dir", path_text(&common), path_text(&primary), ], ); std::fs::write(primary.join("seed"), "seed\n").expect("write seed"); run_git(&primary, &["add", "seed"]); commit_all(&primary, "seed"); run_git( &primary, &[ "worktree", "add", "-b", "nested-unproven-outer", path_text(&outer), ], ); std::fs::create_dir_all(&nested).expect("create nested repository"); run_git(&nested, &["init", "-q"]); write_git_candidate(&trusted_bin); std::fs::create_dir_all(&primary_bin).expect("create primary bin"); std::fs::copy("/usr/bin/touch", primary_bin.join("git")) .expect("copy attacker native executable"); std::fs::remove_file(primary.join(".git")).expect("remove primary reverse marker"); let locations = repository_authority_for_cwd(&nested).expect("untrusted locations"); let path = std::env::join_paths([&primary_bin, &trusted_bin]).expect("PATH"); let error = match GitRunner::from_search_path(locations, &path) { Ok(runner) => { let mut command = runner.command(); command.arg(&executed); let _ = runner.output(command); panic!("unproven enclosing linked repository selected a PATH Git"); } Err(error) => error, }; assert!(!executed.exists(), "attacker Git executed"); assert!(matches!( error, GitReadError::UnprovenPrimaryAuthority { .. } )); } #[test] fn explicit_absolute_core_worktree_is_recorded_before_path_selection() { let fixture = tempdir_for_native_git(); let primary = fixture.path().join("primary"); let common = fixture.path().join("separate-common"); let linked = fixture.path().join("linked"); let primary_bin = primary.join("bin"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&primary).expect("create primary"); run_git( fixture.path(), &[ "init", "--separate-git-dir", path_text(&common), path_text(&primary), ], ); std::fs::write(primary.join("seed"), "seed\n").expect("write seed"); run_git(&primary, &["add", "seed"]); commit_all(&primary, "seed"); run_git( &primary, &[ "worktree", "add", "-b", "explicit-core-worktree-linked", path_text(&linked), ], ); let mut config = std::fs::read_to_string(common.join("config")).expect("read common config"); config.push_str(&format!( "\n[core]\n\tworktree = {}\n", git_config_path(&primary) )); std::fs::write(common.join("config"), config).expect("write explicit core.worktree"); std::fs::remove_file(primary.join(".git")).expect("remove primary reverse marker"); write_git_candidate(&primary_bin); write_git_candidate(&trusted_bin); let locations = repository_authority_for_cwd(&linked).expect("untrusted locations"); assert!( locations.contains_root(&primary), "explicit core.worktree was not recorded" ); assert_eq!( selected_git(&locations, &[&primary_bin, &trusted_bin]), trusted_bin.join(git_executable_name()) ); } #[test] fn resolver_rejects_parent_traversal_spelled_through_repository() { let fixture = tempfile::tempdir().expect("fixture"); let repo = fixture.path().join("repo"); let trusted_bin = fixture.path().join("trusted-bin"); std::fs::create_dir_all(&repo).expect("create repository"); write_git_candidate(&trusted_bin); let locations = locations_for_root(&repo); for root in locations.roots() { // Append without PathBuf::push: it resolves `..` when `root` has a // verbatim Windows prefix, before the resolver can inspect the PATH // spelling. let traversing_path = raw_parent_traversal(root, "trusted-bin"); let search_path = std::env::join_paths([&traversing_path]).expect("PATH"); let split_paths = std::env::split_paths(&search_path).collect::>(); assert_eq!(split_paths, vec![traversing_path.clone()]); assert!( search_directory_is_untrusted(&split_paths[0], &locations), "raw PATH traversal was not rejected from {root:?}" ); assert!( matches!( select_git_executable(&locations, &search_path), Err(GitReadError::NoTrustedGit) ), "resolver accepted parent traversal from {root:?}" ); } } #[cfg(windows)] #[test] fn resolver_rejects_parent_traversal_across_windows_namespaces() { let traversing = [ r"C:\Repo\..\outside", r"\\?\C:\Repo\..\outside", r"\\Server\Share\Repo\..\outside", r"\\?\UNC\Server\Share\Repo\..\outside", r"\\?\unc\Server\Share\Repo\..\outside", r"\\.\C:\Repo\..\outside", r"\\.\UNC\Server\Share\Repo\..\outside", r"\\?\C:\RÉPO\..\outside", ]; for path in traversing { assert!( windows_path_requires_fail_closed(Path::new(path)), "parent traversal was accepted: {path:?}" ); } let normalized_external = [ r"C:\outside", r"\\?\C:\outside", r"\\Server\Share\outside", r"\\?\UNC\Server\Share\outside", r"\\?\unc\Server\Share\outside", r"\\.\C:\outside", r"\\.\UNC\Server\Share\outside", ]; for path in normalized_external { assert!( !windows_path_requires_fail_closed(Path::new(path)), "normalized filesystem path was rejected: {path:?}" ); } } #[cfg(windows)] #[test] fn resolver_rejects_unicode_case_alias_through_repository_junction() { let fixture = tempfile::tempdir().expect("fixture"); let repo = fixture.path().join("Répo"); let outside = fixture.path().join("outside"); let junction = repo.join("git-bin"); std::fs::create_dir_all(&repo).expect("create repository"); write_git_candidate(&outside); create_junction(&junction, &outside); let case_alias = fixture.path().join("RÉPO").join("git-bin"); let verbatim_case_alias = PathBuf::from(format!(r"\\?\{}", case_alias.display())); assert_eq!( std::fs::canonicalize(&verbatim_case_alias).expect("canonical alias"), std::fs::canonicalize(&outside).expect("canonical outside") ); let locations = locations_for_root(&repo); assert!( path_is_untrusted(&verbatim_case_alias, &locations), "route observation missed the Unicode repository alias" ); assert!(search_directory_is_untrusted( &verbatim_case_alias, &locations )); let search_path = std::env::join_paths([verbatim_case_alias]).expect("PATH"); assert!(matches!( GitRunner::from_search_path(locations, &search_path), Err(GitReadError::NoTrustedGit) )); } #[cfg(windows)] #[test] fn resolver_fails_closed_for_unsupported_windows_device_namespaces() { let unsupported = [ r"\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\git.exe", r"\\?\Volume{11111111-1111-1111-1111-111111111111}\git.exe", r"\\.\PhysicalDrive0", r"\\.\pipe\codex-git", ]; for path in unsupported { assert!( windows_path_requires_fail_closed(Path::new(path)), "unsupported namespace was trusted: {path:?}" ); } } #[cfg(windows)] #[test] fn resolver_selects_native_git_exe_only() { let mixed_case = Path::new(r"C:\Repo\.git"); let lower_case = Path::new(r"c:\repo\.GIT"); assert!(crate::git_config::path_is_within(mixed_case, lower_case)); assert!(crate::git_config::path_is_within(lower_case, mixed_case)); let fixture = tempfile::tempdir().expect("fixture"); let repo = fixture.path().join("repo"); let scripts = fixture.path().join("scripts"); let native = fixture.path().join("native"); std::fs::create_dir_all(&repo).expect("repo"); std::fs::create_dir_all(&scripts).expect("scripts"); std::fs::create_dir_all(&native).expect("native"); std::fs::write(scripts.join("git.cmd"), "@exit /b 0\r\n").expect("script"); std::fs::copy(native_git_fixture(), native.join("git.exe")).expect("native executable fixture"); let locations = locations_for_root(&repo); let path = std::env::join_paths([scripts, native.clone()]).expect("PATH"); let runner = GitRunner::from_search_path(locations, &path).expect("native Git"); assert_eq!(runner.argv0, native.join("git.exe")); assert_eq!( runner.executable, std::fs::canonicalize(native.join("git.exe")).expect("canonical native Git") ); }