Skip to content

Commit 0416ba7

Browse files
committed
Resolve separate roots for config and manifest searches
It's no longer assumed that we can resolve a single stop_at_root path for both finding config files and manifests. GlobalContext now maintains a sorted list of user-configured roots and caches two resolved SearchRoutes; one for loading config files, the other for manifests (both automatically invalidated if roots are added or start paths change). A `SearchRoute` represents a starting directory and the closest root directory, where parent traversal will stop. If searching starts from outside of all configured roots then no ancestor traversal is allowed. - This is a safety measure to reduce the risk of loading unsafe config files that aren't owned by the user (e.g. under `c:/.cargo` on Windows or `/tmp` on Linux) By default the user's home directory is added as a root if CARGO_ROOTS is not set. - Combined with the rule above, this means that Cargo has safe defaults and will not load configs outside of your home directory, unless you explicitly set CARGO_ROOTS. - This is also a measure to avoid triggering home directory automounters TODO: there still needs to be a special case for loading a project manifest, before searching for a workspace. Cargo should be allowed to walk ancestors outside of all configured roots, when loading the first Cargo.toml. In this case the directory of the manifest should immediately be set as a root. This would be a safety / ergonomics trade-off to allow building of crates under `/tmp/package/nested/dir` but still disallow attempts to load `/tmp/Cargo.toml` as a workspace. TODO: Cargo should log a warning when searching for files outside of any root, so it's easy to see why ancestor config files or manifests are not being loaded.
1 parent 9ad19bd commit 0416ba7

File tree

11 files changed

+281
-128
lines changed

11 files changed

+281
-128
lines changed

benches/benchsuite/benches/global_cache_tracker.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ fn initialize_context() -> GlobalContext {
3535
let cwd = homedir.clone();
3636
let mut gctx = GlobalContext::new(shell, cwd, homedir);
3737
gctx.nightly_features_allowed = true;
38-
gctx.set_search_stop_path(root());
38+
gctx.set_config_search_root(root());
3939
gctx.configure(
4040
0,
4141
false,

crates/cargo-util/src/paths.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -441,14 +441,14 @@ pub struct PathAncestors<'a> {
441441

442442
impl<'a> PathAncestors<'a> {
443443
fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
444-
let stop_at = env::var("__CARGO_TEST_ROOT")
445-
.ok()
446-
.map(PathBuf::from)
447-
.or_else(|| stop_root_at.map(|p| p.to_path_buf()));
444+
tracing::trace!(
445+
"creating path ancestors iterator from `{}` to `{}`",
446+
path.display(),
447+
stop_root_at.map_or("<root>".to_string(), |p| p.display().to_string())
448+
);
448449
PathAncestors {
449450
current: Some(path),
450-
//HACK: avoid reading `~/.cargo/config` when testing Cargo itself.
451-
stop_at,
451+
stop_at: stop_root_at.map(|p| p.to_path_buf()),
452452
}
453453
}
454454
}
@@ -466,6 +466,7 @@ impl<'a> Iterator for PathAncestors<'a> {
466466
}
467467
}
468468

469+
tracing::trace!("next path ancestor: `{}`", path.display());
469470
Some(path)
470471
} else {
471472
None

src/bin/cargo/cli.rs

Lines changed: 80 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,94 @@ use crate::util::is_rustup;
1818
use cargo::core::shell::ColorChoice;
1919
use cargo::util::style;
2020

21-
fn closest_valid_root<'a>(
22-
cwd: &std::path::Path,
23-
roots: &[&'a std::path::Path],
24-
) -> anyhow::Result<Option<&'a std::path::Path>> {
25-
let cwd = cwd
26-
.canonicalize()
27-
.context("could not canonicalize current working directory")?;
28-
29-
// Assumes that all roots are canonicalized.
30-
let ancestor_roots =
31-
roots
32-
.iter()
33-
.filter_map(|r| if cwd.starts_with(r) { Some(*r) } else { None });
34-
35-
Ok(ancestor_roots.into_iter().max_by_key(|root| {
36-
// Prefer the root that is closest to the current working directory.
37-
// This is done by counting the number of components in the path.
38-
root.components().count()
39-
}))
40-
}
41-
4221
#[tracing::instrument(skip_all)]
4322
pub fn main(gctx: &mut GlobalContext) -> CliResult {
4423
// CAUTION: Be careful with using `config` until it is configured below.
4524
// In general, try to avoid loading config values unless necessary (like
4625
// the [alias] table).
4726

27+
// Register root directories.
28+
//
29+
// A root directory limits how far Cargo can search for files.
30+
//
31+
// Internally there are two notable roots we need to resolve:
32+
// 1. The root when searching for config files (starting directory = cwd).
33+
// 2. The root when searching for manifests (starting directory = manifest-path or cwd directory)
34+
//
35+
// Root directories can be specified via CARGO_ROOTS, --root or the existence of `.cargo/root` files.
36+
//
37+
// If CARGO_ROOTS is not set, the user's home directory is used as a default root.
38+
// - This is a safety measure to avoid reading unsafe config files outside of the user's home
39+
// directory.
40+
// - This is also a measure to avoid triggering home directory automounter issues on some
41+
// systems.
42+
//
43+
// The roots are deduplicated and sorted by their length so we can quickly find the closest root
44+
// to a given starting directory (longest ancestor).
45+
//
46+
// A `SearchRoute` represents a route from a starting directory to the closest root directory.
47+
//
48+
// When there are no roots above a given starting directory, then a `SearchRoute` will use the
49+
// starting directory itself is used as the root.
50+
// - This is a safety measure to avoid reading unsafe config files in unknown locations (such as
51+
// `/tmp`).
52+
//
53+
// There are two cached `SearchRoute`s, one for config files and one for workspace manifests,
54+
// which are used to avoid repeatedly finding the nearest root directory.
55+
56+
// Should it be an error if a given root doesn't exist?
57+
// A user might configure a root under a `/mnt` directory that is not always mounted?
58+
59+
let roots_env = gctx.get_env_os("CARGO_ROOTS").map(|s| s.to_owned());
60+
if let Some(paths_os) = roots_env {
61+
for path in std::env::split_paths(&paths_os) {
62+
gctx.add_root(&path)?;
63+
}
64+
} else {
65+
//HACK: avoid reading `~/.cargo/config` when testing Cargo itself.
66+
let test_root = gctx.get_env_os("__CARGO_TEST_ROOT").map(|s| s.to_owned());
67+
if let Some(test_root) = test_root {
68+
tracing::debug!(
69+
"no CARGO_ROOTS set, using __CARGO_TEST_ROOT as root: {}",
70+
test_root.display()
71+
);
72+
// This is a hack to avoid reading `~/.cargo/config` when testing Cargo itself.
73+
gctx.add_root(&test_root)?;
74+
} else if let Some(home) = std::env::home_dir() {
75+
tracing::debug!(
76+
"no CARGO_ROOTS set, using home directory as root: {}",
77+
home.display()
78+
);
79+
// To be safe by default, and not attempt to read config files outside of the
80+
// user's home directory, we implicitly add the home directory as a root.
81+
// Ref: https://github.com/rust-lang/rfcs/pull/3279
82+
//
83+
// This is also a measure to avoid triggering home directory automounter issues
84+
gctx.add_root(&home)?;
85+
}
86+
}
87+
4888
let args = cli(gctx).try_get_matches()?;
4989

90+
if let Some(cli_roots) = args.get_many::<std::path::PathBuf>("root") {
91+
for cli_root in cli_roots {
92+
gctx.add_root(cli_root)?;
93+
}
94+
}
95+
96+
// Look for any `.cargo/root` markers after we have registered all other roots so
97+
// that other roots can stop us from triggering automounter issues.
98+
let search_route = gctx.find_config_search_route(gctx.cwd());
99+
100+
let root_marker = paths::ancestors(&search_route.start, search_route.root.as_deref())
101+
.find(|current| current.join(".cargo").join("root").exists());
102+
if let Some(marker) = root_marker {
103+
tracing::debug!("found .cargo/root marker at {}", marker.display());
104+
gctx.add_root(marker)?;
105+
} else {
106+
tracing::debug!("no .cargo/root marker found");
107+
}
108+
50109
// Update the process-level notion of cwd
51110
if let Some(new_cwd) = args.get_one::<std::path::PathBuf>("directory") {
52111
// This is a temporary hack.
@@ -70,73 +129,6 @@ pub fn main(gctx: &mut GlobalContext) -> CliResult {
70129
std::env::set_current_dir(&new_cwd).context("could not change to requested directory")?;
71130
}
72131

73-
// A root directories can be specified via CARGO_ROOTS, --root or the existence of a `.cargo/root` files.
74-
// If more than one root is specified, the effective root is the one closest to the current working directory.
75-
// If CARGO_ROOTS is not set, the user's home directory is used as a default root.
76-
77-
let cwd = std::env::current_dir().context("could not get current working directory")?;
78-
// Windows UNC paths are OK here
79-
let cwd = cwd
80-
.canonicalize()
81-
.context("could not canonicalize current working directory")?;
82-
83-
// XXX: before looking for `.cargo/root` should we first try and resolve roots
84-
// from `CARGO_ROOTS` + --root so we can avoid triggering automounter issues?
85-
let root_marker = paths::ancestors(&cwd, gctx.search_stop_path())
86-
.find(|current| current.join(".cargo").join("root").exists());
87-
88-
let mut roots: Vec<std::path::PathBuf> = Vec::new();
89-
90-
if let Some(root_marker) = root_marker {
91-
let pb = root_marker
92-
.canonicalize()
93-
.context("could not canonicalize .cargo/root")?;
94-
roots.push(pb);
95-
}
96-
97-
if let Some(paths_os) = gctx.get_env_os("CARGO_ROOTS") {
98-
for path in std::env::split_paths(&paths_os) {
99-
let pb = path.canonicalize().context(format!(
100-
"could not canonicalize CARGO_ROOTS entry `{}`",
101-
path.display()
102-
))?;
103-
roots.push(pb);
104-
}
105-
} else if let Some(home) = std::env::home_dir() {
106-
// To be safe by default, and not attempt to read config files outside of the
107-
// user's home directory, we implicitly add the home directory as a root.
108-
// Ref: https://github.com/rust-lang/rfcs/pull/3279
109-
let home = home
110-
.canonicalize()
111-
.context("could not canonicalize home directory")?;
112-
tracing::debug!(
113-
"implicitly adding home directory as root: {}",
114-
home.display()
115-
);
116-
roots.push(home);
117-
}
118-
119-
if let Some(cli_roots) = args.get_many::<std::path::PathBuf>("root") {
120-
for cli_root in cli_roots {
121-
let pb = cli_root
122-
.canonicalize()
123-
.context("could not canonicalize requested root directory")?;
124-
roots.push(pb);
125-
}
126-
}
127-
128-
let roots: Vec<_> = roots.iter().map(|p| p.as_path()).collect();
129-
130-
if let Some(root) = closest_valid_root(&cwd, &roots)? {
131-
tracing::debug!("root directory: {}", root.display());
132-
gctx.set_search_stop_path(root);
133-
} else {
134-
tracing::debug!("root limited to cwd: {}", cwd.display());
135-
// If we are not running with _any_ root then we are conservative and don't
136-
// allow any ancestor traversal.
137-
gctx.set_search_stop_path(&cwd);
138-
}
139-
140132
// Reload now that we have established the cwd and root
141133
gctx.reload_cwd()?;
142134

src/cargo/core/workspace.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::core::{
2121
use crate::core::{EitherManifest, Package, SourceId, VirtualManifest};
2222
use crate::ops;
2323
use crate::sources::{PathSource, SourceConfigMap, CRATES_IO_INDEX, CRATES_IO_REGISTRY};
24-
use crate::util::context::FeatureUnification;
24+
use crate::util::context::{FeatureUnification, SearchRoute};
2525
use crate::util::edit_distance;
2626
use crate::util::errors::{CargoResult, ManifestError};
2727
use crate::util::interning::InternedString;
@@ -746,6 +746,7 @@ impl<'gctx> Workspace<'gctx> {
746746
/// Returns an error if `manifest_path` isn't actually a valid manifest or
747747
/// if some other transient error happens.
748748
fn find_root(&mut self, manifest_path: &Path) -> CargoResult<Option<PathBuf>> {
749+
debug!("find_root - {}", manifest_path.display());
749750
let current = self.packages.load(manifest_path)?;
750751
match current
751752
.workspace_config()
@@ -2023,12 +2024,14 @@ fn find_workspace_root_with_loader(
20232024
gctx: &GlobalContext,
20242025
mut loader: impl FnMut(&Path) -> CargoResult<Option<PathBuf>>,
20252026
) -> CargoResult<Option<PathBuf>> {
2027+
let search_route = gctx.find_manifest_search_route(manifest_path);
2028+
20262029
// Check if there are any workspace roots that have already been found that would work
20272030
{
20282031
let roots = gctx.ws_roots.borrow();
20292032
// Iterate through the manifests parent directories until we find a workspace
20302033
// root. Note we skip the first item since that is just the path itself
2031-
for current in paths::ancestors(manifest_path, gctx.search_stop_path()).skip(1) {
2034+
for current in paths::ancestors(&search_route.start, search_route.root.as_deref()).skip(1) {
20322035
if let Some(ws_config) = roots.get(current) {
20332036
if !ws_config.is_excluded(manifest_path) {
20342037
// Add `Cargo.toml` since ws_root is the root and not the file
@@ -2038,7 +2041,7 @@ fn find_workspace_root_with_loader(
20382041
}
20392042
}
20402043

2041-
for ances_manifest_path in find_root_iter(manifest_path, gctx) {
2044+
for ances_manifest_path in find_root_iter(&search_route, gctx) {
20422045
debug!("find_root - trying {}", ances_manifest_path.display());
20432046
if let Some(ws_root_path) = loader(&ances_manifest_path)? {
20442047
return Ok(Some(ws_root_path));
@@ -2058,10 +2061,10 @@ fn read_root_pointer(member_manifest: &Path, root_link: &str) -> PathBuf {
20582061
}
20592062

20602063
fn find_root_iter<'a>(
2061-
manifest_path: &'a Path,
2064+
search_route: &'a SearchRoute,
20622065
gctx: &'a GlobalContext,
20632066
) -> impl Iterator<Item = PathBuf> + 'a {
2064-
LookBehind::new(paths::ancestors(manifest_path, gctx.search_stop_path()).skip(2))
2067+
LookBehind::new(paths::ancestors(&search_route.start, search_route.root.as_deref()).skip(2))
20652068
.take_while(|path| !path.curr.ends_with("target/package"))
20662069
// Don't walk across `CARGO_HOME` when we're looking for the
20672070
// workspace root. Sometimes a package will be organized with

src/cargo/ops/cargo_new.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ fn mk(gctx: &GlobalContext, opts: &MkOptions<'_>) -> CargoResult<()> {
802802
}
803803

804804
let manifest_path = paths::normalize_path(&path.join("Cargo.toml"));
805-
if let Ok(root_manifest_path) = find_root_manifest_for_wd(&manifest_path) {
805+
if let Ok(root_manifest_path) = find_root_manifest_for_wd(gctx, &manifest_path) {
806806
let root_manifest = paths::read(&root_manifest_path)?;
807807
// Sometimes the root manifest is not a valid manifest, so we only try to parse it if it is.
808808
// This should not block the creation of the new project. It is only a best effort to

src/cargo/ops/registry/owner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub fn modify_owners(gctx: &GlobalContext, opts: &OwnersOptions) -> CargoResult<
2828
let name = match opts.krate {
2929
Some(ref name) => name.clone(),
3030
None => {
31-
let manifest_path = find_root_manifest_for_wd(gctx.cwd())?;
31+
let manifest_path = find_root_manifest_for_wd(gctx, gctx.cwd())?;
3232
let ws = Workspace::new(&manifest_path, gctx)?;
3333
ws.current()?.package_id().name().to_string()
3434
}

src/cargo/ops/registry/yank.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub fn yank(
2626
let name = match krate {
2727
Some(name) => name,
2828
None => {
29-
let manifest_path = find_root_manifest_for_wd(gctx.cwd())?;
29+
let manifest_path = find_root_manifest_for_wd(gctx, gctx.cwd())?;
3030
let ws = Workspace::new(&manifest_path, gctx)?;
3131
ws.current()?.package_id().name().to_string()
3232
}

src/cargo/util/command_prelude.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,7 @@ pub fn root_manifest(manifest_path: Option<&Path>, gctx: &GlobalContext) -> Carg
10771077
}
10781078
Ok(path)
10791079
} else {
1080-
find_root_manifest_for_wd(gctx.cwd())
1080+
find_root_manifest_for_wd(gctx, gctx.cwd())
10811081
}
10821082
}
10831083

@@ -1132,7 +1132,7 @@ fn get_profile_candidates() -> Vec<clap_complete::CompletionCandidate> {
11321132

11331133
fn get_workspace_profile_candidates() -> CargoResult<Vec<clap_complete::CompletionCandidate>> {
11341134
let gctx = new_gctx_for_completions()?;
1135-
let ws = Workspace::new(&find_root_manifest_for_wd(gctx.cwd())?, &gctx)?;
1135+
let ws = Workspace::new(&find_root_manifest_for_wd(&gctx, gctx.cwd())?, &gctx)?;
11361136
let profiles = Profiles::new(&ws, InternedString::new("dev"))?;
11371137

11381138
let mut candidates = Vec::new();
@@ -1216,7 +1216,7 @@ fn get_bin_candidates() -> Vec<clap_complete::CompletionCandidate> {
12161216
fn get_targets_from_metadata() -> CargoResult<Vec<Target>> {
12171217
let cwd = std::env::current_dir()?;
12181218
let gctx = GlobalContext::new(shell::Shell::new(), cwd.clone(), cargo_home_with_cwd(&cwd)?);
1219-
let ws = Workspace::new(&find_root_manifest_for_wd(&cwd)?, &gctx)?;
1219+
let ws = Workspace::new(&find_root_manifest_for_wd(&gctx, &cwd)?, &gctx)?;
12201220

12211221
let packages = ws.members().collect::<Vec<_>>();
12221222

@@ -1271,7 +1271,10 @@ fn get_target_triples_from_rustup() -> CargoResult<Vec<clap_complete::Completion
12711271
fn get_target_triples_from_rustc() -> CargoResult<Vec<clap_complete::CompletionCandidate>> {
12721272
let cwd = std::env::current_dir()?;
12731273
let gctx = GlobalContext::new(shell::Shell::new(), cwd.clone(), cargo_home_with_cwd(&cwd)?);
1274-
let ws = Workspace::new(&find_root_manifest_for_wd(&PathBuf::from(&cwd))?, &gctx);
1274+
let ws = Workspace::new(
1275+
&find_root_manifest_for_wd(&gctx, &PathBuf::from(&cwd))?,
1276+
&gctx,
1277+
);
12751278

12761279
let rustc = gctx.load_global_rustc(ws.as_ref().ok())?;
12771280

@@ -1374,7 +1377,7 @@ pub fn get_pkg_id_spec_candidates() -> Vec<clap_complete::CompletionCandidate> {
13741377
fn get_packages() -> CargoResult<Vec<Package>> {
13751378
let gctx = new_gctx_for_completions()?;
13761379

1377-
let ws = Workspace::new(&find_root_manifest_for_wd(gctx.cwd())?, &gctx)?;
1380+
let ws = Workspace::new(&find_root_manifest_for_wd(&gctx, gctx.cwd())?, &gctx)?;
13781381

13791382
let requested_kinds = CompileKind::from_requested_targets(ws.gctx(), &[])?;
13801383
let mut target_data = RustcTargetData::new(&ws, &requested_kinds)?;

0 commit comments

Comments
 (0)