Skip to content

Commit db4bc63

Browse files
committed
syscall: exec_linux: add test for F_DUPFD_CLOEXEC behaviour
1 parent c8c93ef commit db4bc63

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

src/syscall/exec_linux_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package syscall_test
88

99
import (
1010
"bytes"
11+
"errors"
1112
"flag"
1213
"fmt"
1314
"internal/testenv"
@@ -673,3 +674,78 @@ func testAmbientCaps(t *testing.T, userns bool) {
673674
t.Fatal(err.Error())
674675
}
675676
}
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

Comments
 (0)