January 26, 2017·4 min read·engineeringgoruntimeconcurrency

Go 1.7.5 repairs stack handling around select

The maintenance release closes a race between select wakeups and stack growth. Its other fixes show how ordinary language guarantees depend on compiler and runtime bookkeeping.

A blocked select needs the runtime to choose one communication and wake the goroutine once. Growing that goroutine's stack while completing the operation creates a less obvious requirement. Other goroutines must continue to agree about whether a case has already won. Go 1.7.5 changes where the runtime reacquires channel locks after a wakeup to preserve that agreement. The January 26 release also contains compiler and certificate handling corrections.

The most useful account of the select change is in the release's source diff. Its comments describe the failure in terms of stack copying and a shared completion flag. That explanation makes a maintenance release relevant to anyone relying on channel communication, even when the application has no direct interaction with runtime internals.

A winner flag moves with the stack

While the goroutine is asleep, another goroutine can complete one of its select cases. The completing goroutine uses a compare-and-swap operation on a flag to win the right to finish the select. Once that flag records completion, another case must not perform a second communication. The waiting records associated with the select refer to that shared flag.

The source comments explain a problematic interval when the awakened goroutine grows its own stack. Stack copying updates pointers in its waiting records to refer to the new stack. Another goroutine could see the updated pointer before the original flag's value had reached the destination. If the destination still contained zero, a compare-and-swap could incorrectly claim completion a second time.

That second winner could attempt another wakeup. The comments describe a runtime failure when the target goroutine has the stack-copying status. They also describe a worse possibility, a later spurious wakeup when the goroutine is waiting on a semaphore. The problem can consequently affect operations after the original select. The source does not present it as a harmless duplicate notification that application code can ignore.

The dangerous interval is narrow. Stack shrinking already locks the channels involved. Stack growth before parking happens while the channel locks remain held, and growth after reacquiring those locks also excludes competing operations. The uncovered case is growth during the lock acquisition itself. Go 1.7.5 runs that acquisition on the system stack, avoiding growth of the goroutine's own stack during that interval.

I like Go's channel model because ordinary concurrent code can express communication without managing these details itself. That preference makes runtime correctness especially important. A programmer reading a select should be able to reason about its cases without reconstructing an interaction between stack relocation and the scheduler.

Zero values still need compiler work

Another correction concerns partially initialized structure literals used in creating method values. The added regression test defines a structure with three pointer fields and initializes only two. The method checks that the omitted middle field is nil. It exercises a closure calling the method, a method value taken from a literal, and a method value taken from a variable.

The compiler change explicitly zeros a stack temporary used as backing storage. The test and that change together explain the guarantee being restored. Leaving a field out of a literal must not make its value depend on old stack contents. The compiler has to establish the zero value when it prepares the storage, including the path used to construct a method value.

This is a useful distinction between source simplicity and implementation simplicity. A short expression can involve temporary storage that is absent from the source program. Correct behavior still requires every relevant field in that temporary to have the value implied by the program. The release fixes the implementation without requiring a different expression from the caller.

The runtime also changes argument information handling for deferred functions produced through reflection. The diff distinguishes an active call frame from a deferred function that has not yet started executing. In the deferred case, it gets the required information from the function value. A new regression program exercises deferred reflected functions while recursively growing the stack, connecting the bookkeeping change to a concrete execution path.

Maintenance includes the surrounding environment

The Darwin certificate change optimizes the fallback used without cgo. It first discovers certificates with potentially altered trust policies through the security command. It then verifies certificates from that set when assembling roots. The source describes this as a way to avoid invoking certificate verification separately for every system root. The trust-policy discovery and verification remain different steps.

The time change is smaller than a general timekeeping repair. Its test accepts either GMT+1 or -01 as the abbreviation returned for the fixed zone under examination. The expected offset stays negative one hour. A comment attributes the abbreviation difference to tzdata-2016g. This corrects an assumption about the spelling supplied by external timezone data while preserving the offset assertion.

I regard those details as part of maintaining a usable language implementation. The select repair preserves communication behavior, while the timezone test recognizes a legitimate environmental variation. The release's value is specific to those corrections. It gives users on the Go 1.7 line repaired implementations of existing behavior, including code paths that ordinary application syntax does not expose directly.