mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
git-utils: centralize repository authority and trusted Git launch
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
use std::io;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use same_file::Handle;
|
||||
|
||||
use crate::git_config::path_is_within;
|
||||
|
||||
use super::CandidateBoundary;
|
||||
use super::RepositoryRouteBoundaries;
|
||||
use super::classify_candidate;
|
||||
use super::is_missing;
|
||||
use super::route_walker::RouteObservationSnapshot;
|
||||
use super::route_walker::observe_route;
|
||||
#[cfg(windows)]
|
||||
use super::windows_path_is_ambiguous;
|
||||
|
||||
impl RepositoryRouteBoundaries {
|
||||
pub(crate) fn route_cancels_worktree_descendant(&self, route: &Path) -> io::Result<bool> {
|
||||
if route_requires_fail_closed(route)? {
|
||||
return Ok(true);
|
||||
}
|
||||
observation_cancels_worktree_descendant(&observe_route(route)?, self)
|
||||
}
|
||||
|
||||
pub(crate) fn retained_route_is_untrusted(&self, route: &Path) -> io::Result<bool> {
|
||||
if route_requires_fail_closed(route)? {
|
||||
return Ok(true);
|
||||
}
|
||||
let observation = observe_route(route)?;
|
||||
if observation_cancels_worktree_descendant(&observation, self)? {
|
||||
return Ok(true);
|
||||
}
|
||||
// Unlike active metadata, retained registry routes are not rebound
|
||||
// before every child. A symlink or junction entry below a worktree is
|
||||
// therefore mutable authority even when it currently aliases metadata.
|
||||
// Parent identity classification covers Windows Unicode case aliases.
|
||||
for hop in &observation.symlink_hops {
|
||||
if matches!(
|
||||
classify_candidate(
|
||||
&hop.parent,
|
||||
&self.worktree_roots,
|
||||
&self.metadata_dirs,
|
||||
&self.worktree_identities,
|
||||
&self.metadata_identities,
|
||||
)?,
|
||||
CandidateBoundary::ExactWorktree | CandidateBoundary::Worktree
|
||||
) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn observation_cancels_worktree_descendant(
|
||||
observation: &RouteObservationSnapshot,
|
||||
boundaries: &RepositoryRouteBoundaries,
|
||||
) -> io::Result<bool> {
|
||||
if raw_spelling_cancels_worktree_descendant(&observation.raw.spelling, boundaries)? {
|
||||
return Ok(true);
|
||||
}
|
||||
for hop in &observation.symlink_hops {
|
||||
if raw_spelling_cancels_worktree_descendant(&hop.target.spelling, boundaries)?
|
||||
|| raw_spelling_cancels_worktree_descendant(&hop.projected.spelling, boundaries)?
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn route_requires_fail_closed(route: &Path) -> io::Result<bool> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let route = route.to_str().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"non-UTF-8 Windows authority path",
|
||||
)
|
||||
})?;
|
||||
Ok(windows_path_is_ambiguous(route))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = route;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_spelling_cancels_worktree_descendant(
|
||||
spelling: &Path,
|
||||
boundaries: &RepositoryRouteBoundaries,
|
||||
) -> io::Result<bool> {
|
||||
let mut prefix = PathBuf::new();
|
||||
for component in spelling.components() {
|
||||
match component {
|
||||
Component::ParentDir => {
|
||||
if candidate_is_proper_worktree_descendant(
|
||||
&prefix,
|
||||
&boundaries.worktree_roots,
|
||||
&boundaries.metadata_dirs,
|
||||
&boundaries.worktree_identities,
|
||||
&boundaries.metadata_identities,
|
||||
)? {
|
||||
return Ok(true);
|
||||
}
|
||||
prefix.pop();
|
||||
}
|
||||
Component::CurDir => {}
|
||||
_ => prefix.push(component.as_os_str()),
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(super) fn candidate_is_proper_worktree_descendant(
|
||||
path: &Path,
|
||||
worktree_roots: &[PathBuf],
|
||||
metadata_dirs: &[PathBuf],
|
||||
worktree_identities: &[Handle],
|
||||
metadata_identities: &[Handle],
|
||||
) -> io::Result<bool> {
|
||||
if metadata_dirs
|
||||
.iter()
|
||||
.any(|metadata| path_is_within(path, metadata))
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// A worktree-controlled spelling cannot become trusted merely because a
|
||||
// mutable symlink or junction currently aliases protected metadata. Keep
|
||||
// genuinely metadata-spelled paths exempt above, but reject lexical
|
||||
// worktree descendants before following identities.
|
||||
if worktree_roots
|
||||
.iter()
|
||||
.any(|root| path_is_within(path, root) && !path_is_within(root, path))
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let mut ancestor_identities = Vec::new();
|
||||
for (depth, ancestor) in path.ancestors().enumerate() {
|
||||
match std::fs::metadata(ancestor) {
|
||||
Ok(metadata) if metadata.is_dir() => {}
|
||||
Ok(_) => continue,
|
||||
Err(error) if is_missing(&error) => continue,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
let identity = Handle::from_path(ancestor)?;
|
||||
if metadata_identities.contains(&identity) {
|
||||
return Ok(false);
|
||||
}
|
||||
ancestor_identities.push((depth, identity));
|
||||
}
|
||||
|
||||
Ok(ancestor_identities
|
||||
.into_iter()
|
||||
.any(|(depth, identity)| depth > 0 && worktree_identities.contains(&identity)))
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
#[cfg(windows)]
|
||||
use super::windows_path::windows_path_is_ambiguous;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct RawRouteObservation {
|
||||
pub(super) spelling: PathBuf,
|
||||
pub(super) normalized_prefixes: Vec<PathBuf>,
|
||||
pub(super) normalized: PathBuf,
|
||||
pub(super) projected: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct SymlinkRouteObservation {
|
||||
pub(super) entry: PathBuf,
|
||||
pub(super) parent: PathBuf,
|
||||
pub(super) target: RawRouteObservation,
|
||||
pub(super) projected: RawRouteObservation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct RouteObservationSnapshot {
|
||||
pub(super) raw: RawRouteObservation,
|
||||
pub(super) symlink_hops: Vec<SymlinkRouteObservation>,
|
||||
}
|
||||
|
||||
impl RouteObservationSnapshot {
|
||||
pub(super) fn observed_paths(&self) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
append_raw_observation_paths(&mut paths, &self.raw);
|
||||
for hop in &self.symlink_hops {
|
||||
push_unique_spelling(&mut paths, hop.parent.clone());
|
||||
push_unique_spelling(&mut paths, hop.entry.clone());
|
||||
append_raw_observation_paths(&mut paths, &hop.target);
|
||||
append_raw_observation_paths(&mut paths, &hop.projected);
|
||||
}
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SymlinkRouteHop {
|
||||
entry: PathBuf,
|
||||
parent: PathBuf,
|
||||
target: PathBuf,
|
||||
projected: PathBuf,
|
||||
}
|
||||
|
||||
pub(super) fn observe_route(path: &Path) -> io::Result<RouteObservationSnapshot> {
|
||||
let raw = observe_raw_route(path)?;
|
||||
let symlink_hops = symlink_route_hops(path)?
|
||||
.into_iter()
|
||||
.map(|hop| {
|
||||
Ok(SymlinkRouteObservation {
|
||||
entry: hop.entry,
|
||||
parent: hop.parent,
|
||||
target: observe_raw_route(&hop.target)?,
|
||||
projected: observe_raw_route(&hop.projected)?,
|
||||
})
|
||||
})
|
||||
.collect::<io::Result<Vec<_>>>()?;
|
||||
Ok(RouteObservationSnapshot { raw, symlink_hops })
|
||||
}
|
||||
|
||||
fn observe_raw_route(route: &Path) -> io::Result<RawRouteObservation> {
|
||||
let base = route
|
||||
.ancestors()
|
||||
.last()
|
||||
.ok_or_else(|| invalid_data("authority path has no root"))?;
|
||||
let normalized = AbsolutePathBuf::resolve_path_against_base(route, base).into_path_buf();
|
||||
let normalized_prefixes = route
|
||||
.ancestors()
|
||||
.map(|prefix| AbsolutePathBuf::resolve_path_against_base(prefix, base).into_path_buf())
|
||||
.collect();
|
||||
let projected = project_through_longest_existing_ancestor(route)?;
|
||||
Ok(RawRouteObservation {
|
||||
spelling: route.to_path_buf(),
|
||||
normalized_prefixes,
|
||||
normalized,
|
||||
projected,
|
||||
})
|
||||
}
|
||||
|
||||
fn append_raw_observation_paths(paths: &mut Vec<PathBuf>, observation: &RawRouteObservation) {
|
||||
for prefix in &observation.normalized_prefixes {
|
||||
push_unique_spelling(paths, prefix.clone());
|
||||
}
|
||||
push_unique_spelling(paths, observation.projected.clone());
|
||||
}
|
||||
|
||||
fn push_unique_spelling(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
||||
if !paths.iter().any(|existing| existing == &path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn symlink_route_hops(path: &Path) -> io::Result<Vec<SymlinkRouteHop>> {
|
||||
let mut hops = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
collect_symlink_route_hops(path, 0, &mut seen, &mut hops)?;
|
||||
Ok(hops)
|
||||
}
|
||||
|
||||
fn collect_symlink_route_hops(
|
||||
path: &Path,
|
||||
depth: usize,
|
||||
seen: &mut BTreeSet<PathBuf>,
|
||||
hops: &mut Vec<SymlinkRouteHop>,
|
||||
) -> io::Result<()> {
|
||||
if depth > 40 {
|
||||
return Err(invalid_data("authority path symlink cycle"));
|
||||
}
|
||||
if !seen.insert(path.to_path_buf()) {
|
||||
return Ok(());
|
||||
}
|
||||
for ancestor in path.ancestors() {
|
||||
match std::fs::symlink_metadata(ancestor) {
|
||||
Ok(_) => match std::fs::read_link(ancestor) {
|
||||
Ok(target) => {
|
||||
let parent = ancestor
|
||||
.parent()
|
||||
.ok_or_else(|| invalid_data("authority symlink has no parent"))?;
|
||||
let target = resolve_literal_path(target, parent);
|
||||
#[cfg(windows)]
|
||||
if target.to_str().is_none_or(windows_path_is_ambiguous) {
|
||||
return Err(invalid_data("ambiguous Windows authority symlink"));
|
||||
}
|
||||
let suffix = path
|
||||
.strip_prefix(ancestor)
|
||||
.map_err(|_| invalid_data("failed to project authority symlink path"))?;
|
||||
let projected = resolve_literal_path(suffix, &target);
|
||||
hops.push(SymlinkRouteHop {
|
||||
entry: ancestor.to_path_buf(),
|
||||
parent: parent.to_path_buf(),
|
||||
target,
|
||||
projected: projected.clone(),
|
||||
});
|
||||
collect_symlink_route_hops(&projected, depth + 1, seen, hops)?;
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::InvalidInput => {}
|
||||
Err(error) => return Err(error),
|
||||
},
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn project_through_longest_existing_ancestor(path: &Path) -> io::Result<PathBuf> {
|
||||
project_path(path, 0)
|
||||
}
|
||||
|
||||
fn project_path(path: &Path, symlink_depth: usize) -> io::Result<PathBuf> {
|
||||
if symlink_depth > 40 {
|
||||
return Err(invalid_data("too many authority path symlinks"));
|
||||
}
|
||||
if let Ok(canonical) = std::fs::canonicalize(path) {
|
||||
return Ok(canonical);
|
||||
}
|
||||
for ancestor in path.ancestors() {
|
||||
if std::fs::symlink_metadata(ancestor).is_ok()
|
||||
&& let Ok(target) = std::fs::read_link(ancestor)
|
||||
{
|
||||
let parent = ancestor
|
||||
.parent()
|
||||
.ok_or_else(|| invalid_data("authority symlink has no parent"))?;
|
||||
let target = resolve_literal_path(target, parent);
|
||||
#[cfg(windows)]
|
||||
if target.to_str().is_none_or(windows_path_is_ambiguous) {
|
||||
return Err(invalid_data("ambiguous Windows authority symlink"));
|
||||
}
|
||||
let suffix = path
|
||||
.strip_prefix(ancestor)
|
||||
.map_err(|_| invalid_data("failed to project authority path"))?;
|
||||
return project_path(&resolve_literal_path(suffix, &target), symlink_depth + 1);
|
||||
}
|
||||
}
|
||||
for ancestor in path.ancestors() {
|
||||
match std::fs::canonicalize(ancestor) {
|
||||
Ok(canonical) => {
|
||||
let suffix = path
|
||||
.strip_prefix(ancestor)
|
||||
.map_err(|_| invalid_data("failed to project authority path"))?;
|
||||
return Ok(
|
||||
AbsolutePathBuf::resolve_path_against_base(suffix, canonical).into_path_buf(),
|
||||
);
|
||||
}
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Err(invalid_data("authority path has no existing ancestor"))
|
||||
}
|
||||
|
||||
fn resolve_literal_path(path: impl AsRef<Path>, base: &Path) -> PathBuf {
|
||||
let path = path.as_ref();
|
||||
if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
base.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn invalid_data(message: &str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, message)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
pub(crate) fn windows_path_is_ambiguous(path: &str) -> bool {
|
||||
let path = path.replace('/', "\\");
|
||||
if path.starts_with(r"\??\") {
|
||||
return true;
|
||||
}
|
||||
let path = if path
|
||||
.get(..8)
|
||||
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(r"\\?\UNC\"))
|
||||
|| path
|
||||
.get(..8)
|
||||
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(r"\\.\UNC\"))
|
||||
{
|
||||
format!(r"\\{}", &path[8..])
|
||||
} else if path.starts_with(r"\\?\") || path.starts_with(r"\\.\") {
|
||||
let path = &path[4..];
|
||||
if !matches!(path.as_bytes(), [drive, b':', b'\\', ..] if drive.is_ascii_alphabetic()) {
|
||||
return true;
|
||||
}
|
||||
path.to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let bytes = path.as_bytes();
|
||||
let remainder = if matches!(bytes, [drive, b':', separator, ..]
|
||||
if drive.is_ascii_alphabetic() && *separator == b'\\')
|
||||
{
|
||||
&path[3..]
|
||||
} else {
|
||||
path.as_str()
|
||||
};
|
||||
if remainder.contains(':') {
|
||||
return true;
|
||||
}
|
||||
remainder.split('\\').any(|component| {
|
||||
!matches!(component, "." | "..")
|
||||
&& (component.ends_with(['.', ' ']) || windows_reserved_component(component))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn windows_authority_path_is_ambiguous(path: &str) -> bool {
|
||||
windows_path_is_ambiguous(path)
|
||||
|| path
|
||||
.replace('/', "\\")
|
||||
.split('\\')
|
||||
.any(|component| component == "..")
|
||||
}
|
||||
|
||||
fn windows_reserved_component(component: &str) -> bool {
|
||||
let stem = component
|
||||
.trim_end_matches(['.', ' '])
|
||||
.split_once('.')
|
||||
.map_or(component, |(stem, _)| stem);
|
||||
stem.eq_ignore_ascii_case("AUX")
|
||||
|| stem.eq_ignore_ascii_case("CON")
|
||||
|| stem.eq_ignore_ascii_case("CONIN$")
|
||||
|| stem.eq_ignore_ascii_case("CONOUT$")
|
||||
|| stem.eq_ignore_ascii_case("NUL")
|
||||
|| stem.eq_ignore_ascii_case("PRN")
|
||||
|| ["COM", "LPT"].iter().any(|prefix| {
|
||||
let Some(rest) = stem.get(3..) else {
|
||||
return false;
|
||||
};
|
||||
let mut chars = rest.chars();
|
||||
stem.get(..3)
|
||||
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
|
||||
&& matches!(chars.next(), Some('1'..='9' | '¹' | '²' | '³'))
|
||||
&& chars.as_str().is_empty()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user