@@ -2583,90 +2583,154 @@ fn test_install_normal_file_replaces_symlink() {
25832583#[ test]
25842584#[ cfg( unix) ]
25852585fn test_install_d_symlink_race_condition ( ) {
2586- // Test for symlink race condition fix (issue #10013)
2587- // Verifies that pre-existing symlinks in path are handled safely
2586+ // Test that pre-existing symlinks in the path are followed (GNU coreutils behavior).
2587+ // install -D should traverse symlink components rather than replacing them.
25882588 use std:: os:: unix:: fs:: symlink;
25892589
25902590 let scene = TestScenario :: new ( util_name ! ( ) ) ;
25912591 let at = & scene. fixtures ;
25922592
2593- // Create test directories
25942593 at. mkdir ( "target" ) ;
2595-
2596- // Create source file
25972594 at. write ( "source_file" , "test content" ) ;
25982595
2599- // Set up a pre-existing symlink attack scenario
26002596 at. mkdir_all ( "testdir/a" ) ;
2601- let intermediate_dir = at. plus ( "testdir/a/b" ) ;
2602- symlink ( at. plus ( "target" ) , & intermediate_dir) . unwrap ( ) ;
2597+ symlink ( at. plus ( "target" ) , at. plus ( "testdir/a/b" ) ) . unwrap ( ) ;
26032598
2604- // Run install -D which should detect and handle the symlink
2605- let result = scene
2599+ // install -D should follow the symlink and write into the symlink target
2600+ scene
26062601 . ucmd ( )
26072602 . arg ( "-D" )
26082603 . arg ( at. plus ( "source_file" ) )
26092604 . arg ( at. plus ( "testdir/a/b/c/file" ) )
2610- . run ( ) ;
2611-
2612- let wrong_location = at. plus ( "target/c/file" ) ;
2605+ . succeeds ( ) ;
26132606
2614- // The critical assertion: file must NOT be in symlink target (race prevented)
2607+ // File must be written through the symlink, i.e. inside the real target dir
26152608 assert ! (
2616- !wrong_location. exists( ) ,
2617- "RACE CONDITION NOT PREVENTED: File was created in symlink target"
2609+ at. plus( "target/c/file" ) . exists( ) ,
2610+ "File should be written through the symlink into the real target directory"
2611+ ) ;
2612+ assert_eq ! (
2613+ fs:: read_to_string( at. plus( "target/c/file" ) ) . unwrap( ) ,
2614+ "test content"
26182615 ) ;
26192616
2620- // If the command succeeded, verify the file is in the correct location
2621- if result. succeeded ( ) {
2622- assert ! ( at. file_exists( "testdir/a/b/c/file" ) ) ;
2623- assert_eq ! ( at. read( "testdir/a/b/c/file" ) , "test content" ) ;
2624- // The symlink should have been replaced with a real directory
2625- assert ! (
2626- at. plus( "testdir/a/b" ) . is_dir( ) && !at. plus( "testdir/a/b" ) . is_symlink( ) ,
2627- "Intermediate path should be a real directory, not a symlink"
2628- ) ;
2629- }
2617+ // The symlink must not have been replaced with a real directory
2618+ assert ! (
2619+ at. plus( "testdir/a/b" ) . is_symlink( ) ,
2620+ "Intermediate symlink should be preserved, not replaced with a real directory"
2621+ ) ;
26302622}
26312623
26322624#[ test]
26332625#[ cfg( unix) ]
26342626fn test_install_d_symlink_race_condition_concurrent ( ) {
2635- // Test pre-existing symlinks in intermediate paths are handled correctly
2627+ // Verify symlink-following behavior is consistent (companion to the test above).
26362628 use std:: os:: unix:: fs:: symlink;
26372629
26382630 let scene = TestScenario :: new ( util_name ! ( ) ) ;
26392631 let at = & scene. fixtures ;
26402632
2641- // Create test directories and source file using testing framework
26422633 at. mkdir ( "target2" ) ;
26432634 at. write ( "source_file2" , "test content 2" ) ;
26442635
2645- // Set up intermediate directory with symlink
26462636 at. mkdir_all ( "testdir2/a" ) ;
26472637 symlink ( at. plus ( "target2" ) , at. plus ( "testdir2/a/b" ) ) . unwrap ( ) ;
26482638
2649- // Run install -D
26502639 scene
26512640 . ucmd ( )
26522641 . arg ( "-D" )
26532642 . arg ( at. plus ( "source_file2" ) )
26542643 . arg ( at. plus ( "testdir2/a/b/c/file" ) )
26552644 . succeeds ( ) ;
26562645
2657- // Verify file was created at the intended destination
2658- assert ! ( at. file_exists( "testdir2/a/b/c/file" ) ) ;
2659- assert_eq ! ( at. read( "testdir2/a/b/c/file" ) , "test content 2" ) ;
2646+ // File should be in the real target directory (symlink was followed)
2647+ assert ! (
2648+ at. plus( "target2/c/file" ) . exists( ) ,
2649+ "File should be written through the symlink into the real target directory"
2650+ ) ;
2651+ assert_eq ! (
2652+ fs:: read_to_string( at. plus( "target2/c/file" ) ) . unwrap( ) ,
2653+ "test content 2"
2654+ ) ;
26602655
2661- // Verify file was NOT created in symlink target
2656+ // Symlink should be preserved
26622657 assert ! (
2663- ! at. plus( "target2/c/file " ) . exists ( ) ,
2664- "File should NOT be in symlink target location "
2658+ at. plus( "testdir2/a/b " ) . is_symlink ( ) ,
2659+ "Intermediate symlink should be preserved, not replaced with a real directory "
26652660 ) ;
2661+ }
2662+
2663+ #[ test]
2664+ #[ cfg( unix) ]
2665+ fn test_install_d_follows_symlink_prefix ( ) {
2666+ // Regression test for: install -D replaces symlink components instead of following them.
2667+ // Reproduces the exact scenario from the bug report: a symlinked install prefix
2668+ // (common in BOSH, Homebrew, Nix, stow) must be followed, not destroyed.
2669+ use std:: os:: unix:: fs:: symlink;
2670+
2671+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
2672+ let at = & scene. fixtures ;
2673+
2674+ // Simulate: ln -s /tmp/target /tmp/link
2675+ at. mkdir ( "target" ) ;
2676+ symlink ( at. plus ( "target" ) , at. plus ( "link" ) ) . unwrap ( ) ;
2677+
2678+ at. write ( "file.txt" , "hello" ) ;
26662679
2667- // Verify intermediate path is now a real directory
2680+ // install -D -m 644 file.txt link/subdir/file.txt
2681+ scene
2682+ . ucmd ( )
2683+ . args ( & [ "-D" , "-m" , "644" ] )
2684+ . arg ( at. plus ( "file.txt" ) )
2685+ . arg ( at. plus ( "link/subdir/file.txt" ) )
2686+ . succeeds ( ) ;
2687+
2688+ // GNU expected: /tmp/link remains a symlink, file written to /tmp/target/subdir/file.txt
2689+ assert ! (
2690+ at. plus( "link" ) . is_symlink( ) ,
2691+ "The symlinked prefix must remain a symlink"
2692+ ) ;
2693+ assert ! (
2694+ at. plus( "target/subdir/file.txt" ) . exists( ) ,
2695+ "File must be written into the real target directory via the symlink"
2696+ ) ;
2697+ assert_eq ! (
2698+ fs:: read_to_string( at. plus( "target/subdir/file.txt" ) ) . unwrap( ) ,
2699+ "hello"
2700+ ) ;
2701+ }
2702+
2703+ #[ test]
2704+ #[ cfg( unix) ]
2705+ fn test_install_d_dangling_symlink_in_path_errors ( ) {
2706+ // A dangling symlink as a path component must not be silently replaced with a
2707+ // real directory. GNU coreutils errors out in this case; we should too.
2708+ use std:: os:: unix:: fs:: symlink;
2709+
2710+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
2711+ let at = & scene. fixtures ;
2712+
2713+ // Create a symlink pointing to a nonexistent target (dangling)
2714+ symlink ( at. plus ( "nonexistent" ) , at. plus ( "dangling" ) ) . unwrap ( ) ;
2715+ assert ! ( at. plus( "dangling" ) . is_symlink( ) ) ;
2716+
2717+ at. write ( "file.txt" , "hello" ) ;
2718+
2719+ // install -D file.txt dangling/subdir/file.txt should fail
2720+ scene
2721+ . ucmd ( )
2722+ . args ( & [ "-D" , "-m" , "644" ] )
2723+ . arg ( at. plus ( "file.txt" ) )
2724+ . arg ( at. plus ( "dangling/subdir/file.txt" ) )
2725+ . fails ( ) ;
2726+
2727+ // The dangling symlink must not have been replaced with a real directory
2728+ assert ! (
2729+ at. plus( "dangling" ) . is_symlink( ) ,
2730+ "Dangling symlink must not be replaced with a real directory"
2731+ ) ;
26682732 assert ! (
2669- at . plus ( "testdir2/a/b" ) . is_dir ( ) && !at. plus( "testdir2/a/b " ) . is_symlink ( ) ,
2670- "Intermediate directory should be a real directory, not a symlink "
2733+ !at. plus( "nonexistent " ) . exists ( ) ,
2734+ "The symlink target must not have been created "
26712735 ) ;
26722736}
0 commit comments