mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
509 lines
18 KiB
Rust
509 lines
18 KiB
Rust
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 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(unix)]
|
|
write_executable(&candidate, "#!/bin/sh\nexit 0\n");
|
|
#[cfg(windows)]
|
|
std::fs::write(candidate, b"MZ").expect("write native executable fixture");
|
|
#[cfg(not(any(unix, windows)))]
|
|
std::fs::write(candidate, b"git fixture").expect("write executable fixture");
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn create_junction(path: &Path, target: &Path) {
|
|
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) -> UntrustedGitLocations {
|
|
let mut roots = vec![root.to_path_buf()];
|
|
push_unique(
|
|
&mut roots,
|
|
std::fs::canonicalize(root).expect("canonical root"),
|
|
);
|
|
UntrustedGitLocations {
|
|
roots,
|
|
common_dirs: Vec::new(),
|
|
}
|
|
}
|
|
|
|
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 selected_git(locations: &UntrustedGitLocations, directories: &[&Path]) -> PathBuf {
|
|
let search_path = std::env::join_paths(directories).expect("PATH");
|
|
GitRunner::from_search_path(locations, &search_path)
|
|
.expect("trusted Git")
|
|
.executable
|
|
}
|
|
|
|
#[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_executable(&repo_bin.join("git"), "#!/bin/sh\nexit 1\n");
|
|
std::os::unix::fs::symlink(repo_bin.join("git"), outside.join("git"))
|
|
.expect("outside symlink into repository");
|
|
write_executable(&trusted_bin.join("git"), "#!/bin/sh\nprintf 'trusted\\n'\n");
|
|
|
|
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.executable, trusted_bin.join("git"));
|
|
let output = runner.output(runner.command()).expect("run trusted Git");
|
|
assert_eq!(output.stdout, b"trusted\n");
|
|
}
|
|
|
|
#[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 git_dir = main.join(".git/worktrees/linked");
|
|
let main_bin = main.join("bin");
|
|
std::fs::create_dir_all(&git_dir).expect("linked Git directory");
|
|
std::fs::create_dir_all(&linked).expect("linked worktree");
|
|
std::fs::write(
|
|
linked.join(".git"),
|
|
format!("gitdir: {}\n", git_dir.display()),
|
|
)
|
|
.expect("linked .git file");
|
|
write_git_candidate(&main_bin);
|
|
|
|
let locations = untrusted_git_locations_for_cwd(&linked).expect("untrusted locations");
|
|
assert!(path_is_untrusted(
|
|
&main_bin.join(git_executable_name()),
|
|
&locations
|
|
));
|
|
}
|
|
|
|
#[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 = untrusted_git_locations_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 = untrusted_git_locations_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 = untrusted_git_locations_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_git_candidate(&trusted_bin);
|
|
|
|
let locations = untrusted_git_locations_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())
|
|
);
|
|
}
|
|
|
|
#[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_git_candidate(&trusted_bin);
|
|
|
|
let locations = untrusted_git_locations_for_cwd(&linked).expect("untrusted locations");
|
|
assert_eq!(
|
|
selected_git(&locations, &[&trusted_bin]),
|
|
trusted_bin.join(git_executable_name())
|
|
);
|
|
}
|
|
|
|
#[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 = untrusted_git_locations_for_cwd(&linked).expect("untrusted locations");
|
|
assert_eq!(
|
|
selected_git(&locations, &[&main_bin, &malformed_bin, &unrelated_bin]),
|
|
unrelated_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::<Vec<_>>();
|
|
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:?}"
|
|
);
|
|
|
|
#[cfg(windows)]
|
|
if matches!(
|
|
root.components().next(),
|
|
Some(Component::Prefix(prefix)) if prefix.kind().is_verbatim()
|
|
) {
|
|
assert_eq!(
|
|
split_paths[0].join(git_executable_name()),
|
|
std::fs::canonicalize(&trusted_bin)
|
|
.expect("canonical trusted bin")
|
|
.join(git_executable_name())
|
|
);
|
|
}
|
|
|
|
assert!(
|
|
matches!(
|
|
GitRunner::from_search_path(&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),
|
|
"fixture must exercise the Unicode alias before canonical ancestry"
|
|
);
|
|
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() {
|
|
assert!(paths_equal(
|
|
Path::new(r"C:\Repo\.git"),
|
|
Path::new(r"c:\repo\.GIT")
|
|
));
|
|
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::write(native.join("git.exe"), b"MZ").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.executable, native.join("git.exe"));
|
|
}
|