@@ -4,8 +4,10 @@ use crate::fs::{self, File, OpenOptions};
44use crate :: io:: { ErrorKind , SeekFrom } ;
55use crate :: path:: Path ;
66use crate :: str;
7+ use crate :: sync:: Arc ;
78use crate :: sys_common:: io:: test:: { tmpdir, TempDir } ;
89use crate :: thread;
10+ use crate :: time:: { Duration , Instant } ;
911
1012use rand:: { rngs:: StdRng , RngCore , SeedableRng } ;
1113
@@ -602,6 +604,21 @@ fn recursive_rmdir_of_symlink() {
602604 assert ! ( canary. exists( ) ) ;
603605}
604606
607+ #[ test]
608+ fn recursive_rmdir_of_file_fails ( ) {
609+ // test we do not delete a directly specified file.
610+ let tmpdir = tmpdir ( ) ;
611+ let canary = tmpdir. join ( "do_not_delete" ) ;
612+ check ! ( check!( File :: create( & canary) ) . write( b"foo" ) ) ;
613+ let result = fs:: remove_dir_all ( & canary) ;
614+ #[ cfg( unix) ]
615+ error ! ( result, "Not a directory" ) ;
616+ #[ cfg( windows) ]
617+ error ! ( result, 267 ) ; // ERROR_DIRECTORY - The directory name is invalid.
618+ assert ! ( result. is_err( ) ) ;
619+ assert ! ( canary. exists( ) ) ;
620+ }
621+
605622#[ test]
606623// only Windows makes a distinction between file and directory symlinks.
607624#[ cfg( windows) ]
@@ -621,6 +638,59 @@ fn recursive_rmdir_of_file_symlink() {
621638 }
622639}
623640
641+ #[ test]
642+ #[ ignore] // takes too much time
643+ fn recursive_rmdir_toctou ( ) {
644+ // Test for time-of-check to time-of-use issues.
645+ //
646+ // Scenario:
647+ // The attacker wants to get directory contents deleted, to which he does not have access.
648+ // He has a way to get a privileged Rust binary call `std::fs::remove_dir_all()` on a
649+ // directory he controls, e.g. in his home directory.
650+ //
651+ // The POC sets up the `attack_dest/attack_file` which the attacker wants to have deleted.
652+ // The attacker repeatedly creates a directory and replaces it with a symlink from
653+ // `victim_del` to `attack_dest` while the victim code calls `std::fs::remove_dir_all()`
654+ // on `victim_del`. After a few seconds the attack has succeeded and
655+ // `attack_dest/attack_file` is deleted.
656+ let tmpdir = tmpdir ( ) ;
657+ let victim_del_path = tmpdir. join ( "victim_del" ) ;
658+ let victim_del_path_clone = victim_del_path. clone ( ) ;
659+
660+ // setup dest
661+ let attack_dest_dir = tmpdir. join ( "attack_dest" ) ;
662+ let attack_dest_dir = attack_dest_dir. as_path ( ) ;
663+ fs:: create_dir ( attack_dest_dir) . unwrap ( ) ;
664+ let attack_dest_file = tmpdir. join ( "attack_dest/attack_file" ) ;
665+ File :: create ( & attack_dest_file) . unwrap ( ) ;
666+
667+ let drop_canary_arc = Arc :: new ( ( ) ) ;
668+ let drop_canary_weak = Arc :: downgrade ( & drop_canary_arc) ;
669+
670+ eprintln ! ( "x: {:?}" , & victim_del_path) ;
671+
672+ // victim just continuously removes `victim_del`
673+ thread:: spawn ( move || {
674+ while drop_canary_weak. upgrade ( ) . is_some ( ) {
675+ let _ = fs:: remove_dir_all ( & victim_del_path_clone) ;
676+ }
677+ } ) ;
678+
679+ // attacker (could of course be in a separate process)
680+ let start_time = Instant :: now ( ) ;
681+ while Instant :: now ( ) . duration_since ( start_time) < Duration :: from_secs ( 1000 ) {
682+ if !attack_dest_file. exists ( ) {
683+ panic ! (
684+ "Victim deleted symlinked file outside of victim_del. Attack succeeded in {:?}." ,
685+ Instant :: now( ) . duration_since( start_time)
686+ ) ;
687+ }
688+ let _ = fs:: create_dir ( & victim_del_path) ;
689+ let _ = fs:: remove_dir ( & victim_del_path) ;
690+ let _ = symlink_dir ( attack_dest_dir, & victim_del_path) ;
691+ }
692+ }
693+
624694#[ test]
625695fn unicode_path_is_dir ( ) {
626696 assert ! ( Path :: new( "." ) . is_dir( ) ) ;
0 commit comments