@@ -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
@@ -602,6 +604,21 @@ fn recursive_rmdir_of_symlink() {
602
604
assert ! ( canary. exists( ) ) ;
603
605
}
604
606
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
+
605
622
#[ test]
606
623
// only Windows makes a distinction between file and directory symlinks.
607
624
#[ cfg( windows) ]
@@ -621,6 +638,59 @@ fn recursive_rmdir_of_file_symlink() {
621
638
}
622
639
}
623
640
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
+
624
694
#[ test]
625
695
fn unicode_path_is_dir ( ) {
626
696
assert ! ( Path :: new( "." ) . is_dir( ) ) ;
0 commit comments