runtime.LockOSThread()
})
register("LockOSThreadAlt", LockOSThreadAlt)
+
+ registerInit("LockOSThreadAvoidsStatePropagation", func() {
+ // Lock the OS thread now so main runs on the main thread.
+ runtime.LockOSThread()
+ })
+ register("LockOSThreadAvoidsStatePropagation", LockOSThreadAvoidsStatePropagation)
}
func LockOSThreadMain() {
ok:
println("OK")
}
+
+func LockOSThreadAvoidsStatePropagation() {
+ // This test is similar to LockOSThreadAlt in that it will detect if a thread
+ // which should have died is still running. However, rather than do this with
+ // thread IDs, it does this by unsharing state on that thread. This way, it
+ // also detects whether new threads were cloned from the dead thread, and not
+ // from a clean thread. Cloning from a locked thread is undesirable since
+ // cloned threads will inherit potentially unwanted OS state.
+ //
+ // unshareFs, getcwd, and chdir("/tmp") are only guaranteed to work on
+ // Linux, so on other platforms this just checks that the runtime doesn't
+ // do anything terrible.
+ //
+ // This is running locked to the main OS thread.
+
+ // GOMAXPROCS=1 makes this fail much more reliably if a tainted thread is
+ // cloned from.
+ if runtime.GOMAXPROCS(-1) != 1 {
+ println("requires GOMAXPROCS=1")
+ os.Exit(1)
+ }
+
+ if err := chdir("/"); err != nil {
+ println("failed to chdir:", err.Error())
+ os.Exit(1)
+ }
+ // On systems other than Linux, cwd == "".
+ cwd, err := getcwd()
+ if err != nil {
+ println("failed to get cwd:", err.Error())
+ os.Exit(1)
+ }
+ if cwd != "" && cwd != "/" {
+ println("unexpected cwd", cwd, " wanted /")
+ os.Exit(1)
+ }
+
+ ready := make(chan bool, 1)
+ go func() {
+ // This goroutine must be running on a new thread.
+ runtime.LockOSThread()
+
+ // Unshare details about the FS, like the CWD, with
+ // the rest of the process on this thread.
+ // On systems other than Linux, this is a no-op.
+ if err := unshareFs(); err != nil {
+ println("failed to unshare fs:", err.Error())
+ os.Exit(1)
+ }
+ // Chdir to somewhere else on this thread.
+ // On systems other than Linux, this is a no-op.
+ if err := chdir("/tmp"); err != nil {
+ println("failed to chdir:", err.Error())
+ os.Exit(1)
+ }
+
+ // The state on this thread is now considered "tainted", but it
+ // should no longer be observable in any other context.
+
+ ready <- true
+ // Exit with the thread locked.
+ }()
+ <-ready
+
+ // Spawn yet another goroutine and lock it. Since GOMAXPROCS=1, if
+ // for some reason state from the (hopefully dead) locked thread above
+ // propagated into a newly created thread (via clone), or that thread
+ // is actually being re-used, then we should get scheduled on such a
+ // thread with high likelihood.
+ done := make(chan bool)
+ go func() {
+ runtime.LockOSThread()
+
+ // Get the CWD and check if this is the same as the main thread's
+ // CWD. Every thread should share the same CWD.
+ // On systems other than Linux, wd == "".
+ wd, err := getcwd()
+ if err != nil {
+ println("failed to get cwd:", err.Error())
+ os.Exit(1)
+ }
+ if wd != cwd {
+ println("bad state from old thread propagated after it should have died")
+ os.Exit(1)
+ }
+ <-done
+
+ runtime.UnlockOSThread()
+ }()
+ done <- true
+ runtime.UnlockOSThread()
+ println("OK")
+}
--- /dev/null
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build linux
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "syscall"
+)
+
+func gettid() int {
+ return syscall.Gettid()
+}
+
+func tidExists(tid int) (exists, supported bool) {
+ stat, err := ioutil.ReadFile(fmt.Sprintf("/proc/self/task/%d/stat", tid))
+ if os.IsNotExist(err) {
+ return false, true
+ }
+ // Check if it's a zombie thread.
+ state := bytes.Fields(stat)[2]
+ return !(len(state) == 1 && state[0] == 'Z'), true
+}
+
+func getcwd() (string, error) {
+ if !syscall.ImplementsGetwd {
+ return "", nil
+ }
+ // Use the syscall to get the current working directory.
+ // This is imperative for checking for OS thread state
+ // after an unshare since os.Getwd might just check the
+ // environment, or use some other mechanism.
+ var buf [4096]byte
+ n, err := syscall.Getcwd(buf[:])
+ if err != nil {
+ return "", err
+ }
+ // Subtract one for null terminator.
+ return string(buf[:n-1]), nil
+}
+
+func unshareFs() error {
+ return syscall.Unshare(syscall.CLONE_FS)
+}
+
+func chdir(path string) error {
+ return syscall.Chdir(path)
+}