@@ -8,6 +8,7 @@ package syscall_test
8
8
9
9
import (
10
10
"bytes"
11
+ "errors"
11
12
"flag"
12
13
"fmt"
13
14
"internal/testenv"
@@ -673,3 +674,78 @@ func testAmbientCaps(t *testing.T, userns bool) {
673
674
t .Fatal (err .Error ())
674
675
}
675
676
}
677
+
678
+ // Test to ensure that ForkExec doesn't clobber file descriptors not included
679
+ // in cmd.ExtraFiles. See https://golang.org/issue/61751.
680
+ func TestExtraFilesNoClobber (t * testing.T ) {
681
+ testenv .MustHaveExec (t )
682
+
683
+ if os .Getenv ("GO_WANT_HELPER_PROCESS" ) == "1" {
684
+ // Successfully managed to self-exec!
685
+ os .Exit (0 )
686
+ }
687
+
688
+ exe , err := os .Executable ()
689
+ if err != nil {
690
+ t .Fatal (err )
691
+ }
692
+
693
+ // Grab a handle to our /proc/self/exe.
694
+ exeFileOriginal , err := os .Open (exe )
695
+ if err != nil {
696
+ t .Fatal (err )
697
+ }
698
+ defer exeFileOriginal .Close ()
699
+
700
+ // A file we cannot execute.
701
+ devNullOriginal , err := os .Open ("/dev/null" )
702
+ if err != nil {
703
+ t .Fatal (err )
704
+ }
705
+ defer devNullOriginal .Close ()
706
+ if _ , err := exec .LookPath (devNullOriginal .Name ()); err == nil {
707
+ t .Skip ("skipping test -- /dev/null is executable" )
708
+ }
709
+
710
+ // Change the file descriptors such that devNull is a large descriptor and
711
+ // exeFile is one higher. Before https://golang.org/cl/515799, this would
712
+ // cause ForkExec to clobber the descriptor.
713
+ //
714
+ // Unfortunately we can't really have a generic test for clobbering because
715
+ // we can only detect the clobbering of a single file descriptor using this
716
+ // method. It might be possible use {Ptrace: true} to detect clobbering but
717
+ // the behaviour of F_DUPFD_CLOEXEC is not guaranteed (and you never know
718
+ // if some other test has opened a file, throwing off the fd calculations).
719
+ devNullFd := 9000
720
+ exeFileFd := devNullFd + 1
721
+
722
+ if err := syscall .Dup3 (int (devNullOriginal .Fd ()), devNullFd , syscall .O_CLOEXEC ); err != nil {
723
+ t .Fatalf ("dup %s to %d failed: %v" , devNullOriginal .Name (), devNullFd , err )
724
+ }
725
+ devNull := os .NewFile (uintptr (devNullFd ), "/dev/null (dup'd)" )
726
+ defer devNull .Close ()
727
+
728
+ if err := syscall .Dup3 (int (exeFileOriginal .Fd ()), exeFileFd , syscall .O_CLOEXEC ); err != nil {
729
+ t .Fatalf ("dup %s to %d failed: %v" , exeFileOriginal .Name (), exeFileFd , err )
730
+ }
731
+ exeFile := os .NewFile (uintptr (exeFileFd ), exeFileOriginal .Name ()+ " (dup'd)" )
732
+ defer exeFile .Close ()
733
+
734
+ // Try to run exeFile through /proc/self/fd/$n.
735
+ exePath := fmt .Sprintf ("/proc/self/fd/%d" , exeFile .Fd ())
736
+ if _ , err := os .Stat (exePath ); err != nil {
737
+ t .Skipf ("skipping test -- cannot resolve %s" , exePath )
738
+ }
739
+ cmd := testenv .Command (t , exePath , "-test.run=" + t .Name ())
740
+ cmd .Env = append (cmd .Environ (), "GO_WANT_HELPER_PROCESS=1" )
741
+ cmd .Stdout = os .Stdout
742
+ cmd .Stderr = os .Stderr
743
+ cmd .ExtraFiles = []* os.File {devNull }
744
+ if err := cmd .Run (); err != nil {
745
+ if errors .Is (err , os .ErrPermission ) {
746
+ t .Fatalf ("fd %d was clobbered during exec: execve %s: %v" , exeFileFd , exePath , err )
747
+ }
748
+ t .Fatal (err )
749
+ }
750
+ runtime .KeepAlive (exeFile )
751
+ }
0 commit comments