Files
codex/codex-rs/git-utils/src/git_config_sources.rs

141 lines
4.8 KiB
Rust

use std::collections::BTreeSet;
use std::io;
use std::path::Path;
#[cfg(test)]
use std::path::PathBuf;
use crate::git_command::GitRunner;
use crate::git_config::read_config_entries_without_includes;
mod include_graph;
mod path_safety;
mod primary_sources;
use include_graph::validate_include_entries;
use path_safety::normalize_absolute_path;
use path_safety::resolve_literal_path;
use primary_sources::default_system_config_source_candidates;
use primary_sources::is_disabled_primary_config_path;
use primary_sources::legacy_primary_config_source_candidates;
use primary_sources::selected_git_home_config_candidates;
use primary_sources::selected_git_prefix_system_candidate;
const INCLUDE_CONFIG_PATTERN: &str = r"^include(\.path|if\..*\.path)$";
const MAX_CONFIG_INCLUDE_DEPTH: usize = 10;
const MAX_CONFIG_INCLUDE_FILES: usize = 1024;
/// Reject configuration that an untrusted worktree writer can change between
/// a policy probe and the Git command it guards.
pub(crate) fn ensure_no_worktree_config_sources(
git: &GitRunner,
git_root: &Path,
git_config_args: &[String],
) -> io::Result<()> {
let git_root = normalize_absolute_path(std::fs::canonicalize(git_root)?)?;
// Environment, HOME, and XDG paths are attacker-controlled byte strings.
// Classify their exact OsString spelling before `git var` can open or
// newline-delimit one of them.
for (description, candidate) in legacy_primary_config_source_candidates()? {
if !is_disabled_primary_config_path(&candidate) {
reject_source(git, &git_root, &candidate, description)?;
}
}
for candidate in selected_git_home_config_candidates(git, &git_root)? {
reject_source(git, &git_root, &candidate, "selected Git HOME config")?;
}
if let Some(candidate) = selected_git_prefix_system_candidate(git, &git_root)? {
reject_source(
git,
&git_root,
&candidate,
"selected Git prefix system config",
)?;
}
let entries = read_config_entries_without_includes(
git,
&git_root,
git_config_args,
INCLUDE_CONFIG_PATTERN,
"include",
/*config_file*/ None,
)?;
let mut pending = Vec::new();
validate_include_entries(git, &git_root, entries, /*depth*/ 1, &mut pending)?;
let mut visited = BTreeSet::new();
while let Some((config_path, depth)) = pending.pop() {
if depth > MAX_CONFIG_INCLUDE_DEPTH {
return Err(path_safety::invalid_config_source(
"Git config include depth exceeded",
));
}
match std::fs::canonicalize(&config_path) {
Ok(_) => {}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
continue;
}
Err(error) => return Err(error),
}
// The same file reached through two spellings can resolve a relative
// child include differently. Deduplicate only the exact source
// spelling, not its canonical target.
if !visited.insert(config_path.clone()) {
continue;
}
if visited.len() > MAX_CONFIG_INCLUDE_FILES {
return Err(path_safety::invalid_config_source(
"too many Git config include files",
));
}
let entries = read_config_entries_without_includes(
git,
&git_root,
&[],
INCLUDE_CONFIG_PATTERN,
"include",
Some(&config_path),
)?;
validate_include_entries(git, &git_root, entries, depth + 1, &mut pending)?;
}
// `git var` loads repository config before reporting modern default-system
// paths. Delay that supplemental probe until the complete no-includes
// source graph above has been classified, so it cannot be tricked into
// opening an untrusted include or FIFO first.
for (description, candidate) in default_system_config_source_candidates(git, &git_root)? {
if !is_disabled_primary_config_path(&candidate) {
reject_source(git, &git_root, &candidate, description)?;
}
}
Ok(())
}
fn reject_source(
git: &GitRunner,
cwd: &Path,
candidate: &Path,
description: &str,
) -> io::Result<()> {
let absolute = resolve_literal_path(candidate, cwd);
git.ensure_config_source_is_not_worktree_controlled(&absolute, description)
}
#[cfg(test)]
use crate::git_config::GitConfigEntry;
#[cfg(test)]
use codex_utils_absolute_path::AbsolutePathBuf;
#[cfg(test)]
use include_graph::expand_git_config_path;
#[cfg(test)]
use include_graph::resolve_include_path;
#[cfg(test)]
use path_safety::windows_config_path_is_ambiguous;
#[cfg(test)]
#[path = "git_config_sources_tests.rs"]
mod tests;