blob: e9d7fdbc44a0cc9ce0dca8f4d2f7a557effc2ab8 [file] [log] [blame]
// 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("/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")
}
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")
}