mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
371 lines
12 KiB
Rust
371 lines
12 KiB
Rust
use std::ffi::OsStr;
|
|
use std::io;
|
|
#[cfg(windows)]
|
|
use std::path::Component;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
#[cfg(windows)]
|
|
use std::path::Prefix;
|
|
use std::process::Command;
|
|
|
|
use crate::errors::GitReadError;
|
|
use crate::git_config::path_is_within;
|
|
use crate::safe_git::isolate_git_command_environment;
|
|
|
|
/// A Git executable outside the repository-controlled roots for one operation.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct GitRunner {
|
|
executable: PathBuf,
|
|
}
|
|
|
|
struct UntrustedGitLocations {
|
|
roots: Vec<PathBuf>,
|
|
common_dirs: Vec<PathBuf>,
|
|
}
|
|
|
|
impl GitRunner {
|
|
pub(crate) fn for_cwd(cwd: &Path) -> Result<Self, GitReadError> {
|
|
let locations = untrusted_git_locations_for_cwd(cwd)?;
|
|
let search_path = std::env::var_os("PATH").ok_or(GitReadError::NoTrustedGit)?;
|
|
Self::from_search_path(&locations, &search_path)
|
|
}
|
|
|
|
pub(crate) fn for_cwd_io(cwd: &Path) -> io::Result<Self> {
|
|
Self::for_cwd(cwd).map_err(|error| io::Error::new(io::ErrorKind::NotFound, error))
|
|
}
|
|
|
|
pub(crate) fn command(&self) -> Command {
|
|
Command::new(&self.executable)
|
|
}
|
|
|
|
pub(crate) fn output(&self, mut command: Command) -> io::Result<std::process::Output> {
|
|
isolate_git_command_environment(&mut command);
|
|
command.envs(crate::local_only_git_env());
|
|
command.output()
|
|
}
|
|
|
|
fn from_search_path(
|
|
untrusted: &UntrustedGitLocations,
|
|
search_path: &OsStr,
|
|
) -> Result<Self, GitReadError> {
|
|
for directory in std::env::split_paths(search_path) {
|
|
if !directory.is_absolute() {
|
|
continue;
|
|
}
|
|
// Check the raw PATH spelling before canonicalization. A search
|
|
// directory can traverse through an untrusted repository and
|
|
// resolve outside it; checking only the resolved parent or
|
|
// candidate would lose that provenance.
|
|
if search_directory_is_untrusted(&directory, untrusted) {
|
|
continue;
|
|
}
|
|
let candidate = directory.join(git_executable_name());
|
|
if path_is_untrusted(&candidate, untrusted) {
|
|
continue;
|
|
}
|
|
let Ok(canonical_parent) = std::fs::canonicalize(&directory) else {
|
|
continue;
|
|
};
|
|
if path_is_untrusted(&canonical_parent, untrusted) {
|
|
continue;
|
|
}
|
|
let Ok(canonical_candidate) = std::fs::canonicalize(&candidate) else {
|
|
continue;
|
|
};
|
|
if path_is_untrusted(&canonical_candidate, untrusted)
|
|
|| !is_native_executable_file(&canonical_candidate)
|
|
{
|
|
continue;
|
|
}
|
|
return Ok(Self {
|
|
// Preserve multicall spelling because argv[0] may select mode.
|
|
executable: candidate,
|
|
});
|
|
}
|
|
Err(GitReadError::NoTrustedGit)
|
|
}
|
|
}
|
|
|
|
fn untrusted_git_locations_for_cwd(cwd: &Path) -> Result<UntrustedGitLocations, GitReadError> {
|
|
let lexical_cwd = if cwd.is_absolute() {
|
|
cwd.to_path_buf()
|
|
} else {
|
|
std::env::current_dir()
|
|
.map_err(|_| GitReadError::NotRepository {
|
|
path: cwd.to_path_buf(),
|
|
})?
|
|
.join(cwd)
|
|
};
|
|
let canonical_cwd = std::fs::canonicalize(cwd).map_err(|_| GitReadError::NotRepository {
|
|
path: cwd.to_path_buf(),
|
|
})?;
|
|
let worktree_root = crate::get_git_repo_root(&canonical_cwd)
|
|
.and_then(|root| std::fs::canonicalize(root).ok())
|
|
.unwrap_or_else(|| canonical_cwd.clone());
|
|
let mut locations = UntrustedGitLocations {
|
|
roots: Vec::new(),
|
|
common_dirs: Vec::new(),
|
|
};
|
|
record_repository_ancestry(&worktree_root, &mut locations)?;
|
|
|
|
// Canonicalization can erase a repository-controlled symlink prefix. Walk
|
|
// the requested spelling too, deliberately retaining symlink and `..`
|
|
// components so every lexical enclosing checkout remains untrusted.
|
|
let lexical_base = if lexical_cwd.is_dir() {
|
|
lexical_cwd
|
|
} else {
|
|
lexical_cwd
|
|
.parent()
|
|
.ok_or_else(|| GitReadError::NotRepository {
|
|
path: cwd.to_path_buf(),
|
|
})?
|
|
.to_path_buf()
|
|
};
|
|
record_repository_ancestry(&lexical_base, &mut locations)?;
|
|
|
|
// Callers commonly obtain their default cwd from `current_dir()`, which
|
|
// can already have erased a symlink spelling. Recover the standard logical
|
|
// process cwd only when it is absolute and resolves to both the requested
|
|
// cwd and the process cwd. Treating extra roots as untrusted cannot widen
|
|
// executable selection.
|
|
if let Some(logical_cwd) = validated_logical_process_cwd(&canonical_cwd) {
|
|
record_repository_ancestry(&logical_cwd, &mut locations)?;
|
|
}
|
|
Ok(locations)
|
|
}
|
|
|
|
fn validated_logical_process_cwd(canonical_cwd: &Path) -> Option<PathBuf> {
|
|
let process_cwd = std::fs::canonicalize(std::env::current_dir().ok()?).ok()?;
|
|
if !paths_equal(&process_cwd, canonical_cwd) {
|
|
return None;
|
|
}
|
|
let logical_cwd = PathBuf::from(std::env::var_os("PWD")?);
|
|
if !logical_cwd.is_absolute() {
|
|
return None;
|
|
}
|
|
let canonical_logical_cwd = std::fs::canonicalize(&logical_cwd).ok()?;
|
|
paths_equal(&canonical_logical_cwd, canonical_cwd).then_some(logical_cwd)
|
|
}
|
|
|
|
fn record_repository_ancestry(
|
|
start: &Path,
|
|
locations: &mut UntrustedGitLocations,
|
|
) -> Result<(), GitReadError> {
|
|
push_unique(&mut locations.roots, start.to_path_buf());
|
|
record_repository_marker(start, locations)?;
|
|
for ancestor in start.parent().into_iter().flat_map(Path::ancestors) {
|
|
let dot_git = ancestor.join(".git");
|
|
match std::fs::symlink_metadata(&dot_git) {
|
|
Ok(_) => {
|
|
push_unique(&mut locations.roots, ancestor.to_path_buf());
|
|
let canonical_root =
|
|
std::fs::canonicalize(ancestor).map_err(|_| GitReadError::NoTrustedGit)?;
|
|
push_unique(&mut locations.roots, canonical_root.clone());
|
|
record_repository_marker(ancestor, locations)?;
|
|
}
|
|
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
|
Err(_) => return Err(GitReadError::NoTrustedGit),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn record_repository_marker(
|
|
worktree_root: &Path,
|
|
locations: &mut UntrustedGitLocations,
|
|
) -> Result<(), GitReadError> {
|
|
let dot_git = worktree_root.join(".git");
|
|
let common_dir = match std::fs::symlink_metadata(&dot_git) {
|
|
Ok(_) => resolve_common_git_dir(&dot_git).map_err(|()| GitReadError::NoTrustedGit)?,
|
|
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
|
|
Err(_) => return Err(GitReadError::NoTrustedGit),
|
|
};
|
|
if !path_is_within(&common_dir, worktree_root) {
|
|
push_unique(&mut locations.roots, common_dir.clone());
|
|
}
|
|
push_unique(&mut locations.common_dirs, common_dir);
|
|
Ok(())
|
|
}
|
|
|
|
fn push_unique(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
|
if !paths.iter().any(|existing| paths_equal(existing, &path)) {
|
|
paths.push(path);
|
|
}
|
|
}
|
|
|
|
fn path_is_untrusted(path: &Path, locations: &UntrustedGitLocations) -> bool {
|
|
if locations
|
|
.roots
|
|
.iter()
|
|
.any(|root| path_is_within(path, root))
|
|
{
|
|
return true;
|
|
}
|
|
locations
|
|
.common_dirs
|
|
.iter()
|
|
.any(|common_dir| path_is_in_worktree_for_common_dir(path, common_dir))
|
|
}
|
|
|
|
fn search_directory_is_untrusted(directory: &Path, locations: &UntrustedGitLocations) -> bool {
|
|
#[cfg(windows)]
|
|
if windows_path_requires_fail_closed(directory)
|
|
|| windows_path_has_untrusted_canonical_ancestor(directory, locations)
|
|
{
|
|
return true;
|
|
}
|
|
path_is_untrusted(directory, locations)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn windows_path_requires_fail_closed(path: &Path) -> bool {
|
|
let mut components = path.components();
|
|
let supported_namespace = match components.next() {
|
|
Some(Component::Prefix(prefix)) => match prefix.kind() {
|
|
Prefix::Disk(_)
|
|
| Prefix::VerbatimDisk(_)
|
|
| Prefix::UNC(_, _)
|
|
| Prefix::VerbatimUNC(_, _) => true,
|
|
Prefix::DeviceNS(device) => windows_device_namespace_is_filesystem(device),
|
|
Prefix::Verbatim(namespace) => namespace
|
|
.to_str()
|
|
.is_some_and(|namespace| namespace.eq_ignore_ascii_case("UNC")),
|
|
},
|
|
_ => false,
|
|
};
|
|
!supported_namespace || components.any(|component| matches!(component, Component::ParentDir))
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn windows_device_namespace_is_filesystem(device: &OsStr) -> bool {
|
|
let bytes = device.as_encoded_bytes();
|
|
bytes.eq_ignore_ascii_case(b"UNC")
|
|
|| matches!(bytes, [drive, b':'] if drive.is_ascii_alphabetic())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn windows_path_has_untrusted_canonical_ancestor(
|
|
path: &Path,
|
|
locations: &UntrustedGitLocations,
|
|
) -> bool {
|
|
let Ok(canonical_path) = std::fs::canonicalize(path) else {
|
|
return true;
|
|
};
|
|
if path_is_untrusted(&canonical_path, locations) {
|
|
return true;
|
|
}
|
|
for ancestor in path.ancestors().skip(1) {
|
|
let canonical_ancestor = match std::fs::canonicalize(ancestor) {
|
|
Ok(canonical_ancestor) => canonical_ancestor,
|
|
Err(error)
|
|
if matches!(
|
|
error.kind(),
|
|
io::ErrorKind::NotFound | io::ErrorKind::InvalidInput
|
|
) =>
|
|
{
|
|
continue;
|
|
}
|
|
Err(_) => return true,
|
|
};
|
|
if path_is_untrusted(&canonical_ancestor, locations) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn path_is_in_worktree_for_common_dir(path: &Path, expected_common_dir: &Path) -> bool {
|
|
let path = if path.is_dir() {
|
|
path
|
|
} else {
|
|
path.parent().unwrap_or(path)
|
|
};
|
|
for ancestor in path.ancestors() {
|
|
let dot_git = ancestor.join(".git");
|
|
match std::fs::symlink_metadata(&dot_git) {
|
|
Ok(_) => match resolve_common_git_dir(&dot_git) {
|
|
Ok(common_dir) if paths_equal(&common_dir, expected_common_dir) => return true,
|
|
Ok(_) => {}
|
|
Err(()) => return true,
|
|
},
|
|
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
|
Err(_) => return true,
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn paths_equal(left: &Path, right: &Path) -> bool {
|
|
path_is_within(left, right) && path_is_within(right, left)
|
|
}
|
|
|
|
fn resolve_common_git_dir(dot_git: &Path) -> Result<PathBuf, ()> {
|
|
if dot_git.is_dir() {
|
|
return std::fs::canonicalize(dot_git).map_err(|_| ());
|
|
}
|
|
let contents = std::fs::read_to_string(dot_git).map_err(|_| ())?;
|
|
let git_dir = contents
|
|
.trim()
|
|
.strip_prefix("gitdir:")
|
|
.map(str::trim)
|
|
.filter(|path| !path.is_empty())
|
|
.ok_or(())?;
|
|
let git_dir = canonicalize_from(dot_git.parent().ok_or(())?, git_dir)?;
|
|
let commondir = git_dir.join("commondir");
|
|
if commondir.is_file() {
|
|
let common_dir = std::fs::read_to_string(commondir).map_err(|_| ())?;
|
|
let common_dir = common_dir.trim();
|
|
if common_dir.is_empty() {
|
|
return Err(());
|
|
}
|
|
return canonicalize_from(&git_dir, common_dir);
|
|
}
|
|
if git_dir
|
|
.parent()
|
|
.is_some_and(|parent| parent.file_name() == Some(OsStr::new("worktrees")))
|
|
{
|
|
return std::fs::canonicalize(git_dir.parent().and_then(Path::parent).ok_or(())?)
|
|
.map_err(|_| ());
|
|
}
|
|
Ok(git_dir)
|
|
}
|
|
|
|
fn canonicalize_from(base: &Path, path: &str) -> Result<PathBuf, ()> {
|
|
std::fs::canonicalize(base.join(path)).map_err(|_| ())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn git_executable_name() -> &'static str {
|
|
"git.exe"
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
fn git_executable_name() -> &'static str {
|
|
"git"
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn is_native_executable_file(path: &Path) -> bool {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
std::fs::metadata(path)
|
|
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn is_native_executable_file(path: &Path) -> bool {
|
|
path.extension()
|
|
.and_then(OsStr::to_str)
|
|
.is_some_and(|extension| extension.eq_ignore_ascii_case("exe"))
|
|
&& std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
|
|
}
|
|
|
|
#[cfg(not(any(unix, windows)))]
|
|
fn is_native_executable_file(path: &Path) -> bool {
|
|
std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "git_command_tests.rs"]
|
|
mod tests;
|