Skip to content

Commit 6c29ed0

Browse files
authored
Merge pull request #6042 from BenWiederhake/dev-chown-preserve-root
chown+chgrp+chmod: Fix handling of preserve root flag and error messages
2 parents 991d718 + d25d994 commit 6c29ed0

4 files changed

Lines changed: 220 additions & 60 deletions

File tree

src/uu/chmod/src/chmod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ impl Chmoder {
267267
return Err(USimpleError::new(
268268
1,
269269
format!(
270-
"it is dangerous to operate recursively on {}\nuse --no-preserve-root to override this failsafe",
270+
"it is dangerous to operate recursively on {}\nchmod: use --no-preserve-root to override this failsafe",
271271
filename.quote()
272272
)
273273
));

src/uucore/src/lib/features/fs.rs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use libc::{
1313
S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH,
1414
S_IXUSR,
1515
};
16-
use std::borrow::Cow;
1716
use std::collections::HashSet;
1817
use std::collections::VecDeque;
1918
use std::env;
@@ -195,30 +194,6 @@ impl Hash for FileInformation {
195194
}
196195
}
197196

198-
/// resolve a relative path
199-
pub fn resolve_relative_path(path: &Path) -> Cow<Path> {
200-
if path.components().all(|e| e != Component::ParentDir) {
201-
return path.into();
202-
}
203-
let root = Component::RootDir.as_os_str();
204-
let mut result = env::current_dir().unwrap_or_else(|_| PathBuf::from(root));
205-
for comp in path.components() {
206-
match comp {
207-
Component::ParentDir => {
208-
if let Ok(p) = result.read_link() {
209-
result = p;
210-
}
211-
result.pop();
212-
}
213-
Component::CurDir => (),
214-
Component::RootDir | Component::Normal(_) | Component::Prefix(_) => {
215-
result.push(comp.as_os_str());
216-
}
217-
}
218-
}
219-
result.into()
220-
}
221-
222197
/// Controls how symbolic links should be handled when canonicalizing a path.
223198
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224199
pub enum MissingHandling {

src/uucore/src/lib/features/perms.rs

Lines changed: 154 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
//! Common functions to manage permissions
77
8+
// spell-checker:ignore (jargon) TOCTOU
9+
810
use crate::display::Quotable;
911
use crate::error::{strip_errno, UResult, USimpleError};
1012
pub use crate::features::entries;
11-
use crate::fs::resolve_relative_path;
1213
use crate::show_error;
1314
use clap::{Arg, ArgMatches, Command};
1415
use libc::{gid_t, uid_t};
@@ -22,7 +23,7 @@ use std::fs::Metadata;
2223
use std::os::unix::fs::MetadataExt;
2324

2425
use std::os::unix::ffi::OsStrExt;
25-
use std::path::Path;
26+
use std::path::{Path, MAIN_SEPARATOR_STR};
2627

2728
/// The various level of verbosity
2829
#[derive(PartialEq, Eq, Clone, Debug)]
@@ -188,6 +189,75 @@ pub struct ChownExecutor {
188189
pub dereference: bool,
189190
}
190191

192+
#[cfg(test)]
193+
pub fn check_root(path: &Path, would_recurse_symlink: bool) -> bool {
194+
is_root(path, would_recurse_symlink)
195+
}
196+
197+
/// In the context of chown and chgrp, check whether we are in a "preserve-root" scenario.
198+
///
199+
/// In particular, we want to prohibit further traversal only if:
200+
/// (--preserve-root and -R present) &&
201+
/// (path canonicalizes to "/") &&
202+
/// (
203+
/// (path is a symlink && would traverse/recurse this symlink) ||
204+
/// (path is not a symlink)
205+
/// )
206+
/// The first clause is checked by the caller, the second and third clause is checked here.
207+
/// The caller has to evaluate -P/-H/-L into 'would_recurse_symlink'.
208+
/// Recall that canonicalization resolves both relative paths (e.g. "..") and symlinks.
209+
fn is_root(path: &Path, would_traverse_symlink: bool) -> bool {
210+
// The third clause can be evaluated without any syscalls, so we do that first.
211+
// If we would_recurse_symlink, then the clause is true no matter whether the path is a symlink
212+
// or not. Otherwise, we only need to check here if the path can syntactically be a symlink:
213+
if !would_traverse_symlink {
214+
// We cannot check path.is_dir() here, as this would resolve symlinks,
215+
// which we need to avoid here.
216+
// All directory-ish paths match "*/", except ".", "..", "*/.", and "*/..".
217+
let looks_like_dir = match path.as_os_str().to_str() {
218+
// If it contains special character, prefer to err on the side of safety, i.e. forbidding the chown operation:
219+
None => false,
220+
Some(".") | Some("..") => true,
221+
Some(path_str) => {
222+
(path_str.ends_with(MAIN_SEPARATOR_STR))
223+
|| (path_str.ends_with(&format!("{}.", MAIN_SEPARATOR_STR)))
224+
|| (path_str.ends_with(&format!("{}..", MAIN_SEPARATOR_STR)))
225+
}
226+
};
227+
// TODO: Once we reach MSRV 1.74.0, replace this abomination by something simpler, e.g. this:
228+
// let path_bytes = path.as_os_str().as_encoded_bytes();
229+
// let looks_like_dir = path_bytes == [b'.']
230+
// || path_bytes == [b'.', b'.']
231+
// || path_bytes.ends_with(&[MAIN_SEPARATOR as u8])
232+
// || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.'])
233+
// || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.', b'.']);
234+
if !looks_like_dir {
235+
return false;
236+
}
237+
}
238+
239+
// FIXME: TOCTOU bug! canonicalize() runs at a different time than WalkDir's recursion decision.
240+
// However, we're forced to make the decision whether to warn about --preserve-root
241+
// *before* even attempting to chown the path, let alone doing the stat inside WalkDir.
242+
if let Ok(p) = path.canonicalize() {
243+
let path_buf = path.to_path_buf();
244+
if p.parent().is_none() {
245+
if path_buf.as_os_str() == "/" {
246+
show_error!("it is dangerous to operate recursively on '/'");
247+
} else {
248+
show_error!(
249+
"it is dangerous to operate recursively on {} (same as '/')",
250+
path_buf.quote()
251+
);
252+
}
253+
show_error!("use --no-preserve-root to override this failsafe");
254+
return true;
255+
}
256+
}
257+
258+
false
259+
}
260+
191261
impl ChownExecutor {
192262
pub fn exec(&self) -> UResult<()> {
193263
let mut ret = 0;
@@ -217,31 +287,12 @@ impl ChownExecutor {
217287
}
218288
};
219289

220-
// Prohibit only if:
221-
// (--preserve-root and -R present) &&
222-
// (
223-
// (argument is not symlink && resolved to be '/') ||
224-
// (argument is symlink && should follow argument && resolved to be '/')
225-
// )
226-
if self.recursive && self.preserve_root {
227-
let may_exist = if self.dereference {
228-
path.canonicalize().ok()
229-
} else {
230-
let real = resolve_relative_path(path);
231-
if real.is_dir() {
232-
Some(real.canonicalize().expect("failed to get real path"))
233-
} else {
234-
Some(real.into_owned())
235-
}
236-
};
237-
238-
if let Some(p) = may_exist {
239-
if p.parent().is_none() {
240-
show_error!("it is dangerous to operate recursively on '/'");
241-
show_error!("use --no-preserve-root to override this failsafe");
242-
return 1;
243-
}
244-
}
290+
if self.recursive
291+
&& self.preserve_root
292+
&& is_root(path, self.traverse_symlinks != TraverseSymlinks::None)
293+
{
294+
// Fail-fast, do not attempt to recurse.
295+
return 1;
245296
}
246297

247298
let ret = if self.matched(meta.uid(), meta.gid()) {
@@ -332,6 +383,12 @@ impl ChownExecutor {
332383
}
333384
};
334385

386+
if self.preserve_root && is_root(path, self.traverse_symlinks == TraverseSymlinks::All)
387+
{
388+
// Fail-fast, do not recurse further.
389+
return 1;
390+
}
391+
335392
if !self.matched(meta.uid(), meta.gid()) {
336393
self.print_verbose_ownership_retained_as(
337394
path,
@@ -586,3 +643,73 @@ pub fn chown_base(
586643
};
587644
executor.exec()
588645
}
646+
647+
#[cfg(test)]
648+
mod tests {
649+
// Note this useful idiom: importing names from outer (for mod tests) scope.
650+
use super::*;
651+
#[cfg(unix)]
652+
use std::os::unix;
653+
use std::path::{Component, Path, PathBuf};
654+
#[cfg(unix)]
655+
use tempfile::tempdir;
656+
657+
#[test]
658+
fn test_empty_string() {
659+
let path = PathBuf::new();
660+
assert_eq!(path.to_str(), Some(""));
661+
// The main point to test here is that we don't crash.
662+
// The result should be 'false', to avoid unnecessary and confusing warnings.
663+
assert_eq!(false, is_root(&path, false));
664+
assert_eq!(false, is_root(&path, true));
665+
}
666+
667+
#[cfg(unix)]
668+
#[test]
669+
fn test_literal_root() {
670+
let component = Component::RootDir;
671+
let path: &Path = component.as_ref();
672+
assert_eq!(
673+
path.to_str(),
674+
Some("/"),
675+
"cfg(unix) but using non-unix path delimiters?!"
676+
);
677+
// Must return true, this is the main scenario that --preserve-root shall prevent.
678+
assert_eq!(true, is_root(&path, false));
679+
assert_eq!(true, is_root(&path, true));
680+
}
681+
682+
#[cfg(unix)]
683+
#[test]
684+
fn test_symlink_slash() {
685+
let temp_dir = tempdir().unwrap();
686+
let symlink_path = temp_dir.path().join("symlink");
687+
unix::fs::symlink(&PathBuf::from("/"), &symlink_path).unwrap();
688+
let symlink_path_slash = temp_dir.path().join("symlink/");
689+
// Must return true, we're about to "accidentally" recurse on "/",
690+
// since "symlink/" always counts as an already-entered directory
691+
// Output from GNU:
692+
// $ chown --preserve-root -RH --dereference $(id -u) slink-to-root/
693+
// chown: it is dangerous to operate recursively on 'slink-to-root/' (same as '/')
694+
// chown: use --no-preserve-root to override this failsafe
695+
// [$? = 1]
696+
// $ chown --preserve-root -RH --no-dereference $(id -u) slink-to-root/
697+
// chown: it is dangerous to operate recursively on 'slink-to-root/' (same as '/')
698+
// chown: use --no-preserve-root to override this failsafe
699+
// [$? = 1]
700+
assert_eq!(true, is_root(&symlink_path_slash, false));
701+
assert_eq!(true, is_root(&symlink_path_slash, true));
702+
}
703+
704+
#[cfg(unix)]
705+
#[test]
706+
fn test_symlink_no_slash() {
707+
// This covers both the commandline-argument case and the recursion case.
708+
let temp_dir = tempdir().unwrap();
709+
let symlink_path = temp_dir.path().join("symlink");
710+
unix::fs::symlink(&PathBuf::from("/"), &symlink_path).unwrap();
711+
// Only return true we're about to "accidentally" recurse on "/".
712+
assert_eq!(false, is_root(&symlink_path, false));
713+
assert_eq!(true, is_root(&symlink_path, true));
714+
}
715+
}

tests/by-util/test_chgrp.rs

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,29 @@ fn test_fail_silently() {
8585
#[test]
8686
fn test_preserve_root() {
8787
// It's weird that on OS X, `realpath /etc/..` returns '/private'
88+
new_ucmd!()
89+
.arg("--preserve-root")
90+
.arg("-R")
91+
.arg("bin")
92+
.arg("/")
93+
.fails()
94+
.stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n");
8895
for d in [
89-
"/",
9096
"/////dev///../../../../",
9197
"../../../../../../../../../../../../../../",
9298
"./../../../../../../../../../../../../../../",
9399
] {
100+
let expected_error = format!(
101+
"chgrp: it is dangerous to operate recursively on '{}' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n",
102+
d,
103+
);
94104
new_ucmd!()
95105
.arg("--preserve-root")
96106
.arg("-R")
97-
.arg("bin").arg(d)
107+
.arg("bin")
108+
.arg(d)
98109
.fails()
99-
.stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n");
110+
.stderr_is(expected_error);
100111
}
101112
}
102113

@@ -105,17 +116,24 @@ fn test_preserve_root_symlink() {
105116
let file = "test_chgrp_symlink2root";
106117
for d in [
107118
"/",
119+
"//",
120+
"///",
108121
"////dev//../../../../",
109122
"..//../../..//../..//../../../../../../../../",
110123
".//../../../../../../..//../../../../../../../",
111124
] {
112125
let (at, mut ucmd) = at_and_ucmd!();
113126
at.symlink_file(d, file);
127+
let expected_error = format!(
128+
"chgrp: it is dangerous to operate recursively on 'test_chgrp_symlink2root' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n",
129+
//d,
130+
);
114131
ucmd.arg("--preserve-root")
115132
.arg("-HR")
116-
.arg("bin").arg(file)
133+
.arg("bin")
134+
.arg(file)
117135
.fails()
118-
.stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n");
136+
.stderr_is(expected_error);
119137
}
120138

121139
let (at, mut ucmd) = at_and_ucmd!();
@@ -124,15 +142,55 @@ fn test_preserve_root_symlink() {
124142
.arg("-HR")
125143
.arg("bin").arg(format!(".//{file}/..//..//../../"))
126144
.fails()
127-
.stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n");
145+
.stderr_is("chgrp: it is dangerous to operate recursively on './/test_chgrp_symlink2root/..//..//../../' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n");
128146

129147
let (at, mut ucmd) = at_and_ucmd!();
130148
at.symlink_file("/", "__root__");
131149
ucmd.arg("--preserve-root")
132150
.arg("-R")
133151
.arg("bin").arg("__root__/.")
134152
.fails()
135-
.stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n");
153+
.stderr_is("chgrp: it is dangerous to operate recursively on '__root__/.' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n");
154+
}
155+
156+
#[test]
157+
fn test_preserve_root_symlink_cwd_root() {
158+
new_ucmd!()
159+
.current_dir("/")
160+
.arg("--preserve-root")
161+
.arg("-R")
162+
.arg("bin").arg(".")
163+
.fails()
164+
.stderr_is("chgrp: it is dangerous to operate recursively on '.' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n");
165+
new_ucmd!()
166+
.current_dir("/")
167+
.arg("--preserve-root")
168+
.arg("-R")
169+
.arg("bin").arg("/.")
170+
.fails()
171+
.stderr_is("chgrp: it is dangerous to operate recursively on '/.' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n");
172+
new_ucmd!()
173+
.current_dir("/")
174+
.arg("--preserve-root")
175+
.arg("-R")
176+
.arg("bin").arg("..")
177+
.fails()
178+
.stderr_is("chgrp: it is dangerous to operate recursively on '..' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n");
179+
new_ucmd!()
180+
.current_dir("/")
181+
.arg("--preserve-root")
182+
.arg("-R")
183+
.arg("bin").arg("/..")
184+
.fails()
185+
.stderr_is("chgrp: it is dangerous to operate recursively on '/..' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n");
186+
new_ucmd!()
187+
.current_dir("/")
188+
.arg("--preserve-root")
189+
.arg("-R")
190+
.arg("bin")
191+
.arg("...")
192+
.fails()
193+
.stderr_is("chgrp: cannot access '...': No such file or directory\n");
136194
}
137195

138196
#[test]

0 commit comments

Comments
 (0)