@@ -27,6 +27,7 @@ use walkdir::WalkDir;
2727
2828#[ cfg( target_os = "linux" ) ]
2929use crate :: features:: fs:: FileInformation ;
30+ use crate :: features:: fs:: path_is_root_dir;
3031#[ cfg( target_os = "linux" ) ]
3132use crate :: features:: safe_traversal:: { DirFd , SymlinkBehavior } ;
3233
@@ -37,7 +38,7 @@ use std::io::Result as IOResult;
3738use std:: os:: unix:: fs:: MetadataExt ;
3839
3940use std:: os:: unix:: ffi:: OsStrExt ;
40- use std:: path:: { MAIN_SEPARATOR , Path } ;
41+ use std:: path:: Path ;
4142
4243#[ derive( Debug , Error ) ]
4344enum PermsError {
@@ -224,56 +225,37 @@ pub fn check_root(path: &Path, would_recurse_symlink: bool) -> bool {
224225
225226/// In the context of chown and chgrp, check whether we are in a "preserve-root" scenario.
226227///
227- /// In particular, we want to prohibit further traversal only if:
228+ /// Prohibit further traversal only if:
228229/// (--preserve-root and -R present) &&
229- /// (path canonicalizes to "/") &&
230+ /// (path *is* "/" by (st_dev, st_ino), so a bind mount of "/" counts too ) &&
230231/// (
231232/// (path is a symlink && would traverse/recurse this symlink) ||
232233/// (path is not a symlink)
233234/// )
234- /// The first clause is checked by the caller, the second and third clause is checked here.
235+ /// The first clause is checked by the caller, the second and third here.
235236/// The caller has to evaluate -P/-H/-L into 'would_recurse_symlink'.
236- /// Recall that canonicalization resolves both relative paths (e.g. "..") and symlinks.
237237fn is_root ( path : & Path , would_traverse_symlink : bool ) -> bool {
238- // The third clause can be evaluated without any syscalls, so we do that first.
239- // If we would_recurse_symlink, then the clause is true no matter whether the path is a symlink
240- // or not. Otherwise, we only need to check here if the path can syntactically be a symlink:
241- if !would_traverse_symlink {
242- // We cannot check path.is_dir() here, as this would resolve symlinks,
243- // which we need to avoid here.
244- // All directory-ish paths match "*/", except ".", "..", "*/.", and "*/..".
245- let path_bytes = path. as_os_str ( ) . as_encoded_bytes ( ) ;
246- let looks_like_dir = path_bytes == * b"."
247- || path_bytes == * b".."
248- || path_bytes. ends_with ( & [ MAIN_SEPARATOR as u8 ] )
249- || path_bytes. ends_with ( & [ MAIN_SEPARATOR as u8 , b'.' ] )
250- || path_bytes. ends_with ( & [ MAIN_SEPARATOR as u8 , b'.' , b'.' ] ) ;
251-
252- if !looks_like_dir {
253- return false ;
254- }
238+ // Compare by (st_dev, st_ino), not name: a bind mount of "/" is an ordinary
239+ // directory whose path never resolves to "/", so the old syntactic "looks
240+ // like a directory?" pre-filter waved it through. `would_traverse_symlink`
241+ // says whether a symlink to "/" here would be followed (only then is it root).
242+ //
243+ // FIXME: TOCTOU bug! This stat runs at a different time than the recursion
244+ // decision it guards; GNU avoids the window by reusing fts's `struct stat`.
245+ if !path_is_root_dir ( path, would_traverse_symlink) {
246+ return false ;
255247 }
256248
257- // FIXME: TOCTOU bug! canonicalize() runs at a different time than WalkDir's recursion decision.
258- // However, we're forced to make the decision whether to warn about --preserve-root
259- // *before* even attempting to chown the path, let alone doing the stat inside WalkDir.
260- if let Ok ( p) = path. canonicalize ( ) {
261- let path_buf = path. to_path_buf ( ) ;
262- if p. parent ( ) . is_none ( ) {
263- if path_buf. as_os_str ( ) == "/" {
264- show_error ! ( "it is dangerous to operate recursively on '/'" ) ;
265- } else {
266- show_error ! (
267- "it is dangerous to operate recursively on {} (same as '/')" ,
268- path_buf. quote( )
269- ) ;
270- }
271- show_error ! ( "use --no-preserve-root to override this failsafe" ) ;
272- return true ;
273- }
249+ if path. as_os_str ( ) == "/" {
250+ show_error ! ( "it is dangerous to operate recursively on '/'" ) ;
251+ } else {
252+ show_error ! (
253+ "it is dangerous to operate recursively on {} (same as '/')" ,
254+ path. quote( )
255+ ) ;
274256 }
275-
276- false
257+ show_error ! ( "use --no-preserve-root to override this failsafe" ) ;
258+ true
277259}
278260
279261pub fn get_metadata ( file : & Path , follow : bool ) -> std:: io:: Result < Metadata > {
0 commit comments