// 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. package main import ( "os" "runtime" "sync" "time" ) var mainTID int func init() { registerInit("LockOSThreadMain", func() { // init is guaranteed to run on the main thread. mainTID = gettid() }) register("LockOSThreadMain", LockOSThreadMain) registerInit("LockOSThreadAlt", func() { // Lock the OS thread now so main runs on the main thread. 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) register("LockOSThreadTemplateThreadRace", LockOSThreadTemplateThreadRace) } func LockOSThreadMain() { // gettid only works on Linux, so on other platforms this just // checks that the runtime doesn't do anything terrible. // This requires GOMAXPROCS=1 from the beginning to reliably // start a goroutine on the main thread. if runtime.GOMAXPROCS(-1) != 1 { println("requires GOMAXPROCS=1") os.Exit(1) } ready := make(chan bool, 1) go func() { // Because GOMAXPROCS=1, this *should* be on the main // thread. Stay there. runtime.LockOSThread() if mainTID != 0 && gettid() != mainTID { println("failed to start goroutine on main thread") os.Exit(1) } // Exit with the thread locked, which should exit the // main thread. ready <- true }() <-ready time.Sleep(1 * time.Millisecond) // Check that this goroutine is still running on a different // thread. if mainTID != 0 && gettid() == mainTID { println("goroutine migrated to locked thread") os.Exit(1) } println("OK") } func LockOSThreadAlt() { // This is running locked to the main OS thread. var subTID int ready := make(chan bool, 1) go func() { // This goroutine must be running on a new thread. runtime.LockOSThread() subTID = gettid() ready <- true // Exit with the thread locked. }() <-ready runtime.UnlockOSThread() for i := 0; i < 100; i++ { time.Sleep(1 * time.Millisecond) // Check that this goroutine is running on a different thread. if subTID != 0 && gettid() == subTID { println("locked thread reused") os.Exit(1) } exists, supported := tidExists(subTID) if !supported || !exists { goto ok } } println("sub thread", subTID, "still running") return 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 { if err == errNotPermitted { println("unshare not permitted") os.Exit(0) } 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(os.TempDir()); 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") } func LockOSThreadTemplateThreadRace() { // This test attempts to reproduce the race described in // golang.org/issue/38931. To do so, we must have a stop-the-world // (achieved via ReadMemStats) racing with two LockOSThread calls. // // While this test attempts to line up the timing, it is only expected // to fail (and thus hang) around 2% of the time if the race is // present. // Ensure enough Ps to actually run everything in parallel. Though on // <4 core machines, we are still at the whim of the kernel scheduler. runtime.GOMAXPROCS(4) go func() { // Stop the world; race with LockOSThread below. var m runtime.MemStats for { runtime.ReadMemStats(&m) } }() // Try to synchronize both LockOSThreads. start := time.Now().Add(10 * time.Millisecond) var wg sync.WaitGroup wg.Add(2) for i := 0; i < 2; i++ { go func() { for time.Now().Before(start) { } // Add work to the local runq to trigger early startm // in handoffp. go func() {}() runtime.LockOSThread() runtime.Gosched() // add a preemption point. wg.Done() }() } wg.Wait() // If both LockOSThreads completed then we did not hit the race. println("OK") }