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